Skip to content

Commit

Permalink
Flux CLI (#1070)
Browse files Browse the repository at this point in the history
* Prosopo flux package initial

* add readme

* drop unused env

* Add getDapps command

* Update readme

* Make authentication generic

* Command to get individual dapp details

* Update README

* Update Readme header

* Update readme login link

* Update node login instructions

* Update README headers and lint

* terminal wip

* Successfully connecting to socket

* Create function for socket URL

* Use logger not console

* update

* remove npmignore

* Add logger
  • Loading branch information
forgetso authored Mar 1, 2024
1 parent 3d35dae commit f74a5c6
Show file tree
Hide file tree
Showing 25 changed files with 966 additions and 287 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ jobs:
run: |
if [[ "${{ steps.publish_docker_js_server.outcome }}" == 'success' ]]; then
echo "Redeploying flux docker js_server."
npx tsx ./dev/scripts/dist/scripts/fluxDeploy.js ProcaptchaJavascriptServer
npx tsx ./dev/flux/dist/index.js deploy --app ProcaptchaJavascriptServer
else
echo "Skipping flux redeploy."
exit 1
Expand Down
95 changes: 95 additions & 0 deletions dev/flux/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# @prosopo/flux

This package contains a Command Line Interface (CLI) for interacting with the Flux network.

## CLI Usage

### Authentication

#### Authenticate with Flux Main Site only

Use to generate the authentication for a Flux node for a specific app.This allows you to go straight
to the [RunOnFlux Login](https://cloud.runonflux.io/login.html) and login with the login phrase and signature.

```bash
# authenticate with the network for a specific app
npm run cli auth
{
nodeAPIURL: URL {
href: 'https://api.runonflux.io/',
origin: 'https://api.runonflux.io',
...
},
nodeLoginPhrase: 'asdlkadalkdalskdasldkadlkadlkaldkasdlk',
nodeSignature: 'lkjasdlajsdlkajdlkajdlaskjdlaskjdlaskjdaslkd/9I='
}

```

#### Authenticate with a Node and an App

Use to generate the authentication for a Flux node for a specific app. This allows you to go straight to the node for
the app you are working with and login with the login phrase and signature.

```bash
# authenticate with the network for a specific app
npm run cli auth -- --app <app_name>
{
nodeAPIURL: URL {
# You can now visit this (using port 16126) to login with the login phrase and signature
href: 'http://x.x.x.x:16127/',
origin: 'http://x.x.x.x:16127',
...
},
nodeLoginPhrase: 'asdlkadalkdalskdasldkadlkadlkaldkasdlk',
nodeSignature: 'lkjasdlajsdlkajdlkajdlaskjdlaskjdlaskjdaslkd/9I='
}
```

### Get Dapp Details

#### Get Dapps

Use this command to get a list of all the dapps that are running on the network.

```bash
# get all dapps
npm run cli getDapps
```

#### Get Dapp

Use this command to get the IP addresses of the nodes that are running the app.

```bash
# get details of a specific dapp
npm run cli getDapp -- --app <app_name>
```

### Redeploy

#### Soft redeploy

Restart the app on all nodes that are running the app.

```bash
# redeploy the app
npm run cli redeploy -- --app <app_name>
...
ℹ apiUrl: http://x.x.x.x:16127/id/verifylogin deploy.js 15:00:42
ℹ { status: 'success', deploy.js 15:00:42
data: { message: '<app_name> queried for global soft redeploy' } }
```
#### Hard redeploy
Remove the container and restart the app on all nodes that are running the app.
```bash
# hard redeploy the app
npm run cli redeploy -- --app <app_name> --hard
...
ℹ apiUrl: http://x.x.x.x:16127/id/verifylogin deploy.js 15:00:42
ℹ { status: 'success', deploy.js 15:00:42
data: { message: '<app_name> queried for global hard redeploy' } }
```
4 changes: 4 additions & 0 deletions dev/flux/env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
NODE_ENV=development
PROSOPO_LOG_LEVEL=debug
PROSOPO_ZELCORE_PRIVATE_KEY=
PROSOPO_ZELCORE_PUBLIC_KEY=
51 changes: 51 additions & 0 deletions dev/flux/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@prosopo/flux",
"version": "0.2.41",
"description": "Tools for managing Flux deployment",
"main": "dist/index.js",
"type": "module",
"engines": {
"node": ">=18",
"npm": ">=9"
},
"scripts": {
"clean": "tsc --build --clean",
"build": "tsc --build --verbose",
"test": "NODE_OPTIONS=--max-old-space-size=4096 npx vitest run --config ./src/vite.config.ts",
"cli": "node dist/index.js",
"eslint": "npx eslint . --no-error-on-unmatched-pattern --ignore-path ../../.eslintignore",
"eslint:fix": "npm run eslint -- --fix",
"prettier": "npx prettier . --check --no-error-on-unmatched-pattern --ignore-path ../../.eslintignore",
"prettier:fix": "npm run prettier -- --write",
"lint": "npm run eslint && npm run prettier",
"lint:fix": "npm run eslint:fix && npm run prettier:fix"
},
"author": "Prosopo Limited",
"license": "Apache-2.0",
"dependencies": {
"@noble/curves": "^1.3.0",
"@polkadot/util": "12.6.1",
"@polkadot/util-crypto": "12.6.1",
"@prosopo/cli": "0.2.41",
"@prosopo/common": "0.2.41",
"@prosopo/util": "0.2.41",
"consola": "^3.2.3",
"dotenv": "^16.0.3",
"glob": "^10.0.0",
"qs": "^6.11.2",
"socket.io-client": "^4.7.4",
"varuint-bitcoin": "^1.1.2",
"yargs": "^17.5.1",
"yargs-parser": "^21.0.1"
},
"overrides": {
"@polkadot/keyring": "12.6.1"
},
"devDependencies": {
"@esm-bundle/chai": "^4.3.4-fix.0",
"ts-node": "^10.9.1",
"tslib": "2.6.2",
"typescript": "5.1.6",
"vitest": "^0.34.2"
}
}
43 changes: 43 additions & 0 deletions dev/flux/src/commands/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as z from 'zod'
import { ArgumentsCamelCase, Argv } from 'yargs'
import { LogLevel, Logger, getLogger } from '@prosopo/common'
import { getPrivateKey, getPublicKey } from './process.env.js'
import { main } from '../lib/auth.js'

const fluxAuthArgs = z.object({
app: z.string().optional(),
ip: z.string().optional(),
})

export default (cmdArgs?: { logger?: Logger }) => {
const logger = cmdArgs?.logger || getLogger(LogLevel.enum.info, 'flux.cli.auth')

return {
command: 'auth',
describe: 'Authenticate with a Flux Node',
builder: (yargs: Argv) =>
yargs
.option('app', {
type: 'string' as const,
demandOption: false,
desc: 'Name of the app to authenticate with',
} as const)
.option('ip', {
type: 'string' as const,
demandOption: false,
desc: 'IP address of Flux machine to authenticate with',
} as const),
handler: async (argv: ArgumentsCamelCase) => {
try {
const privateKey = getPrivateKey()
const publicKey = getPublicKey()
const parsedArgs = fluxAuthArgs.parse(argv)
const result = await main(publicKey, privateKey, parsedArgs.app, parsedArgs.ip)
logger.info(result)
} catch (err) {
logger.error(err)
}
},
middlewares: [],
}
}
37 changes: 37 additions & 0 deletions dev/flux/src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as z from 'zod'
import { ArgumentsCamelCase, Argv } from 'yargs'
import { LogLevel, Logger, getLogger } from '@prosopo/common'
import { getPrivateKey, getPublicKey } from './process.env.js'
import { main } from '../lib/deploy.js'

const fluxDeployArgs = z.object({
app: z.string(),
ip: z.string().optional(),
hard: z.boolean().optional(),
})

export default (cmdArgs?: { logger?: Logger }) => {
const logger = cmdArgs?.logger || getLogger(LogLevel.enum.info, 'flux.cli.deploy')

return {
command: 'deploy',
describe: 'Deploy a Flux application',
builder: (yargs: Argv) =>
yargs.option('hard', {
type: 'boolean' as const,
demand: false,
desc: 'Whether to hard redeploy the app',
} as const),
handler: async (argv: ArgumentsCamelCase) => {
try {
const privateKey = getPrivateKey()
const publicKey = getPublicKey()
const parsedArgs = fluxDeployArgs.parse(argv)
await main(publicKey, privateKey, parsedArgs.app, parsedArgs.ip, parsedArgs.hard)
} catch (err) {
logger.error(err)
}
},
middlewares: [],
}
}
35 changes: 35 additions & 0 deletions dev/flux/src/commands/getDapp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as z from 'zod'
import { ArgumentsCamelCase, Argv } from 'yargs'
import { LogLevel, Logger, getLogger } from '@prosopo/common'
import { getAuth, getIndividualFluxAppDetails } from '../lib/auth.js'
import { getPrivateKey, getPublicKey } from './process.env.js'
const fluxGetDappArgs = z.object({
app: z.string(),
})
export default (cmdArgs?: { logger?: Logger }) => {
const logger = cmdArgs?.logger || getLogger(LogLevel.enum.info, 'flux.cli.getDapp')

return {
command: 'getDapp',
describe: 'Get dapp details',
builder: (yargs: Argv) =>
yargs.option('app', {
type: 'string' as const,
demandOption: false,
desc: 'Name of the dapp to get the details of',
} as const),
handler: async (argv: ArgumentsCamelCase) => {
try {
const privateKey = getPrivateKey()
const publicKey = getPublicKey()
const { signature, loginPhrase } = await getAuth(privateKey)
const parsedArgs = fluxGetDappArgs.parse(argv)
const dapp = await getIndividualFluxAppDetails(parsedArgs.app, publicKey, signature, loginPhrase)
logger.info(JSON.stringify(dapp, null, 2))
} catch (err) {
logger.error(err)
}
},
middlewares: [],
}
}
23 changes: 23 additions & 0 deletions dev/flux/src/commands/getDapps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { LogLevel, Logger, getLogger } from '@prosopo/common'
import { getPrivateKey, getPublicKey } from './process.env.js'
import { main } from '../lib/getDapps.js'

export default (cmdArgs?: { logger?: Logger }) => {
const logger = cmdArgs?.logger || getLogger(LogLevel.enum.info, 'flux.cli.getDapps')

return {
command: 'getDapps',
describe: 'Get dapp details',
handler: async () => {
try {
const privateKey = getPrivateKey()
const publicKey = getPublicKey()
const dapps = await main(publicKey, privateKey)
logger.info(JSON.stringify(dapps, null, 2))
} catch (err) {
logger.error(err)
}
},
middlewares: [],
}
}
5 changes: 5 additions & 0 deletions dev/flux/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { default as commandDeploy } from './deploy.js'
export { default as commandAuth } from './auth.js'
export { default as commandGetDapps } from './getDapps.js'
export { default as commandGetDapp } from './getDapp.js'
export { default as commandTerminal } from './terminal.js'
23 changes: 23 additions & 0 deletions dev/flux/src/commands/process.env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ProsopoEnvError } from '@prosopo/common'
import { wifToPrivateKey } from '../lib/sep256k1Sign.js'

export const getPrivateKey = () => {
const secret = process.env['PROSOPO_ZELCORE_PRIVATE_KEY']
if (!secret) {
throw new ProsopoEnvError('DEVELOPER.MISSING_ENV_VARIABLE', {
context: { missingEnvVars: ['PROSOPO_ZELCORE_PRIVATE_KEY'] },
})
}
return wifToPrivateKey(secret)
}

export const getPublicKey = () => {
const secret = process.env['PROSOPO_ZELCORE_PUBLIC_KEY']
if (!secret) {
throw new ProsopoEnvError('DEVELOPER.MISSING_ENV_VARIABLE', {
context: { missingEnvVars: ['PROSOPO_ZELCORE_PUBLIC_KEY'] },
})
}

return secret
}
43 changes: 43 additions & 0 deletions dev/flux/src/commands/terminal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as z from 'zod'
import { ArgumentsCamelCase, Argv } from 'yargs'
import { LogLevel, Logger, getLogger } from '@prosopo/common'
import { getPrivateKey, getPublicKey } from './process.env.js'
import { main } from '../lib/terminal.js'

const fluxAuthArgs = z.object({
app: z.string(),
ip: z.string().optional(),
})

export default (cmdArgs?: { logger?: Logger }) => {
const logger = cmdArgs?.logger || getLogger(LogLevel.enum.info, 'flux.cli.auth')

return {
command: 'terminal',
describe: 'Start a terminal to a Flux Node',
builder: (yargs: Argv) =>
yargs
.option('app', {
type: 'string' as const,
demandOption: false,
desc: 'Name of the app to authenticate with',
} as const)
.option('ip', {
type: 'string' as const,
demandOption: false,
desc: 'IP address of Flux machine to authenticate with',
} as const),
handler: async (argv: ArgumentsCamelCase) => {
try {
const privateKey = getPrivateKey()
const publicKey = getPublicKey()
const parsedArgs = fluxAuthArgs.parse(argv)
const result = await main(publicKey, privateKey, parsedArgs.app, parsedArgs.ip)
logger.info(result)
} catch (err) {
logger.error(err)
}
},
middlewares: [],
}
}
21 changes: 21 additions & 0 deletions dev/flux/src/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ProsopoApiError } from '@prosopo/common'
export async function streamToJson(stream: ReadableStream<Uint8Array>): Promise<Record<any, any>> {
return await new Response(stream).json()
}

export const errorHandler = async <T>(response: Response) => {
if (!response.ok) {
throw new ProsopoApiError('API.BAD_REQUEST', { context: { error: `HTTP error! status: ${response.status}` } })
}
if (response.body && !response.bodyUsed) {
const data = await streamToJson(response.body)

if (data.status === 'error') {
throw new ProsopoApiError('API.BAD_REQUEST', {
context: { error: `HTTP error! status: ${data.data.message} ` },
})
}
return data as T
}
return {} as T
}
Loading

0 comments on commit f74a5c6

Please sign in to comment.