From 4838007184ec997e50f651a7e3c403f89eaf758f Mon Sep 17 00:00:00 2001 From: Aitor <1726644+aaitor@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:57:19 +0200 Subject: [PATCH] feat: management of NVM API Keys --- package.json | 4 +- resources/commands.json | 35 ++++++++ resources/networks.json | 14 +++- src/commands/app/createApiKey.ts | 133 +++++++++++++++++++++++++++++++ src/commands/app/listApiKeys.ts | 77 ++++++++++++++++++ src/commands/app/revokeApiKey.ts | 55 +++++++++++++ src/commands/index.ts | 3 + src/models/ConfigDefinition.ts | 4 +- src/utils/config.ts | 1 + src/utils/utils.ts | 5 ++ 10 files changed, 325 insertions(+), 6 deletions(-) create mode 100644 src/commands/app/createApiKey.ts create mode 100644 src/commands/app/listApiKeys.ts create mode 100644 src/commands/app/revokeApiKey.ts diff --git a/package.json b/package.json index 4fcbce5..4b09790 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nevermined-io/cli", - "version": "2.2.1", + "version": "2.2.2", "main": "index.js", "repository": "git@github.com:nevermined-io/cli.git", "author": "Nevermined", @@ -25,7 +25,7 @@ "ncli": "./dist/src/index.js" }, "dependencies": { - "@nevermined-io/sdk": "3.0.29", + "@nevermined-io/sdk": "3.0.33", "log4js": "^6.9.1", "chalk": "^4.1.2", "cross-fetch": "~3.1.5", diff --git a/resources/commands.json b/resources/commands.json index eb7fad3..4b3e935 100644 --- a/resources/commands.json +++ b/resources/commands.json @@ -2264,6 +2264,41 @@ "hidden": true, "description": "The NFT type" }] + }, { + "name": "create-api-key [name]", + "description": "It creates a new NVM API Key", + "details": "Nevermined API Keys allow to interact with the protocol in a programatic way. This command allows the generation of NVM API Keys. NOTE: This method requires the user to setup the ZERO_PROJECT_ID environment variable.", + "examples": ["ncli app create-api-key 'My Test API Key'"], + "commandHandler": "createApiKey", + "requiresAccount": true, + "positionalArguments": [{ + "name": "name", + "type": "string", + "description": "The name of the NVM API Key to create" + }], + "optionalArguments": [] + }, { + "name": "list-api-keys", + "description": "It lists all the NVM API Keys associated to the user", + "details": "This command lists allt he NVM API Keys created by the user.", + "examples": ["ncli app list-api-keys"], + "commandHandler": "listApiKeys", + "requiresAccount": true, + "positionalArguments": [], + "optionalArguments": [] + }, { + "name": "revoke-api-key [hash]", + "description": "It revokes a existing NVM API Key", + "details": "Nevermined API Keys allow to interact with the protocol in a programatic way. This command allows to revoke an existing NVM API Keys.", + "examples": ["ncli app revoke-api-key 'eyJhbGciOiJFUzI1NksifQ.eyJpc'"], + "commandHandler": "revokeApiKey", + "requiresAccount": true, + "positionalArguments": [{ + "name": "hash", + "type": "string", + "description": "The hash of the NVM API Key to revoke" + }], + "optionalArguments": [] }] } ] diff --git a/resources/networks.json b/resources/networks.json index 3dd90de..00eb277 100644 --- a/resources/networks.json +++ b/resources/networks.json @@ -11,6 +11,7 @@ "graphHttpUri": "http://localhost:9000/subgraphs/name/nevermined-io/development", "neverminedNodeUri": "http://node.nevermined.localnet", "neverminedNodeAddress": "0x068ed00cf0441e4829d9784fcbe7b9e26d4bd8d0", + "neverminedBackendUri": "http://one-backend.nevermined.localnet", "verbose": true }, "nativeToken": "ETH", @@ -34,6 +35,7 @@ "graphHttpUri": "", "neverminedNodeUri": "https://node.staging.nevermined.app", "neverminedNodeAddress": "0x5838B5512cF9f12FE9f2beccB20eb47211F9B0bc", + "neverminedBackendUri": "https://one-backend.staging.nevermined.app", "verbose": true }, "nativeToken": "ETH", @@ -59,6 +61,7 @@ "graphHttpUri": "https://api.thegraph.com/subgraphs/name/nevermined-io/public", "neverminedNodeUri": "https://node.matic.nevermined.app", "neverminedNodeAddress": "0x824dbcE5E9C96C5b8ce2A35a25a5ab87eD1D00b1", + "neverminedBackendUri": "https://one-backend.matic.nevermined.app", "verbose": true }, "nativeToken": "MATIC", @@ -84,6 +87,7 @@ "graphHttpUri": "", "neverminedNodeUri": "https://node.testing.nevermined.app", "neverminedNodeAddress": "0x5838B5512cF9f12FE9f2beccB20eb47211F9B0bc", + "neverminedBackendUri": "https://one-backend.testing.nevermined.app", "verbose": true }, "nativeToken": "ETH", @@ -109,6 +113,7 @@ "graphHttpUri": "https://api.thegraph.com/subgraphs/name/nevermined-io/public", "neverminedNodeUri": "https://node.arbitrum.nevermined.app", "neverminedNodeAddress": "0x824dbcE5E9C96C5b8ce2A35a25a5ab87eD1D00b1", + "neverminedBackendUri": "https://one-backend.arbitrum.nevermined.app", "verbose": true }, "nativeToken": "ETH", @@ -134,6 +139,7 @@ "graphHttpUri": "https://api.thegraph.com/subgraphs/name/nevermined-io/public", "neverminedNodeUri": "https://node.base.nevermined.app", "neverminedNodeAddress": "0x824dbcE5E9C96C5b8ce2A35a25a5ab87eD1D00b1", + "neverminedBackendUri": "https://one-backend.base.nevermined.app", "verbose": true }, "nativeToken": "ETH", @@ -159,6 +165,7 @@ "graphHttpUri": "https://api.thegraph.com/subgraphs/name/nevermined-io/public", "neverminedNodeUri": "https://node.gnosis.nevermined.app", "neverminedNodeAddress": "0x824dbcE5E9C96C5b8ce2A35a25a5ab87eD1D00b1", + "neverminedBackendUri": "https://one-backend.gnosis.nevermined.app", "verbose": true }, "nativeToken": "xDAI", @@ -184,6 +191,7 @@ "graphHttpUri": "https://api.thegraph.com/subgraphs/name/nevermined-io/public", "neverminedNodeUri": "https://node.optimism.nevermined.app", "neverminedNodeAddress": "0x824dbcE5E9C96C5b8ce2A35a25a5ab87eD1D00b1", + "neverminedBackendUri": "https://one-backend.optimism.nevermined.app", "verbose": true }, "nativeToken": "ETH", @@ -209,6 +217,7 @@ "graphHttpUri": "", "neverminedNodeUri": "http://node.celo.nevermined.app", "neverminedNodeAddress": "0x824dbcE5E9C96C5b8ce2A35a25a5ab87eD1D00b1", + "neverminedBackendUri": "https://one-backend.celo.nevermined.app", "verbose": true }, "nativeToken": "CELO", @@ -232,13 +241,14 @@ "web3ProviderUri": "https://evm.peaq.network", "marketplaceUri": "https://marketplace-api.peaq.nevermined.app", "graphHttpUri": "", - "neverminedNodeUri": "http://node.peaq.nevermined.app", + "neverminedNodeUri": "https://node.peaq.nevermined.app", "neverminedNodeAddress": "0x824dbcE5E9C96C5b8ce2A35a25a5ab87eD1D00b1", + "neverminedBackendUri": "https://one-backend.peaq.nevermined.app", "verbose": true }, "nativeToken": "PEAQ", "networkName": "peaq-mainnet", - "contractsVersion": "3.5.7", + "contractsVersion": "3.5.8", "tagName": "public", "etherscanUrl": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fmpfn1.peaq.network#/explorer", "erc20TokenAddress": "0x0000000000000000000000000000000000000000", diff --git a/src/commands/app/createApiKey.ts b/src/commands/app/createApiKey.ts new file mode 100644 index 0000000..4ee3242 --- /dev/null +++ b/src/commands/app/createApiKey.ts @@ -0,0 +1,133 @@ +import { NvmAccount, NvmApp, getFullZeroDevPermissions, createSessionKey, NvmApiKey } from '@nevermined-io/sdk' +import { + StatusCodes, + getNeverminedContractAbi, +} from '../../utils' +import chalk from 'chalk' +import { ExecutionOutput } from '../../models/ExecutionOutput' +import { Logger } from 'log4js' +import { ConfigEntry } from '../../models/ConfigDefinition' +import { createPublicClient, http, toHex } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' + + +export const createApiKey = async ( + nvmApp: NvmApp, + publisherAccount: NvmAccount, + argv: any, + config: ConfigEntry, + logger: Logger +): Promise => { + + const ZERO_PROJECT_ID = process.env.ZERO_PROJECT_ID || undefined + if (!ZERO_PROJECT_ID) { + return { + status: StatusCodes.ERROR, + errorMessage: `Missing environment variable ZERO_PROJECT_ID` + } + } + + const NVM_BACKEND_URL = config.nvm.neverminedBackendUri || undefined + if (!NVM_BACKEND_URL) { + return { + status: StatusCodes.ERROR, + errorMessage: `Missing Nevermined Backend URL` + } + } + + const BUNDLER_RPC = `https://rpc.zerodev.app/api/v2/bundler/${ZERO_PROJECT_ID}` + + const publicClient = createPublicClient({ + chain: nvmApp.sdk.client.chain, + transport: http(BUNDLER_RPC), + }) + logger.debug(`Public client created with chain: ${nvmApp.sdk.client.chain?.name}`) + logger.debug(BUNDLER_RPC) + + logger.info(chalk.dim(`Creating a new NVM API Key ...`)) + + + const networkName = config.networkName || 'geth-localnet' + const didRegistryAbi = getNeverminedContractAbi('DIDRegistry', networkName) + const nftSalesTemplateAbi = getNeverminedContractAbi('NFTSalesTemplate', networkName) + const nftPlansAbi = getNeverminedContractAbi('NFT1155SubscriptionUpgradeable', networkName) + + logger.debug(`Generating permissions for the API Key ...`) + const permissions = getFullZeroDevPermissions( + didRegistryAbi.address, // DIDRegistry address + nftSalesTemplateAbi.address, // Sales Template address + config.erc20TokenAddress as `0x${string}`, // ERC20 address + nftPlansAbi.address, // NFT1155 address + nftPlansAbi.address, + ) + + logger.debug(`Permissions generated for network: ${networkName}`) + logger.debug(`DIDRegistry: ${didRegistryAbi.address}`) + logger.debug(`NFTSalesTemplate: ${nftSalesTemplateAbi.address}`) + logger.debug(`NFTPlansAbi: ${nftPlansAbi.address}`) + logger.debug(`ERC20 Address: ${config.erc20TokenAddress}`) + // logger.debug(JSON.stringify(permissions)) + + + logger.debug(`Creating a new ZeroDev kernel client ...`) + logger.debug(`Chain ID: ${nvmApp.sdk.client.chain?.id}`) + + const privateKey = toHex(publisherAccount.getAccountSigner().getHdKey().privateKey) + + const account = privateKeyToAccount(privateKey) + logger.debug(`Private key Account: ${account.address}`) + + logger.debug(`Creating a new ZeroDev session key via account: ${account.address} ...`) + const sessionKey = await createSessionKey(account, publicClient, permissions) + + logger.debug(`Generating encrypted NVM API Key ...`) + const encryptedNvmApiKey = await NvmApiKey.generate( + nvmApp.sdk.utils.signature, + publisherAccount, + sessionKey, + config.nvm.marketplaceAuthToken!, + config.nvm.neverminedNodeAddress!, + await nvmApp.sdk.services.node.getEcdsaPublicKey() + ) + // logger.debug(encryptedNvmApiKey) + + logger.debug(`Registering NVM API Key ...`) + const options = { + method: 'POST', + headers: { + 'Content-type': 'application/json' + }, + body: JSON.stringify({ + name: argv.name, + nvmKey: encryptedNvmApiKey + }) + } + + // logger.debug(`POST ${NVM_BACKEND_URL} with options`) + // logger.debug(JSON.parse(options.body)) + const _result = await fetch(`${NVM_BACKEND_URL}/api/v1/api-keys`, options) + if (_result.status !== 200 && _result.status !== 201) { + logger.error(`Error registering NVM API Key: ${_result.statusText}`) + logger.error(await _result.text()) + return { + status: StatusCodes.ERROR, + errorMessage: `Error registering NVM API Key: ${_result.statusText}` + } + } + + logger.info(chalk.green(`NVM API Key created successfully`)) + const keyResult = await _result.json() + logger.info(chalk.dim(`API Key Wallet: ${chalk.yellow(keyResult.userWallet)}`)) + logger.info(chalk.dim(`API Key UserId: ${chalk.yellow(keyResult.userId)}`)) + logger.info(chalk.dim(`API Key Hash: ${chalk.yellow(keyResult.hash)}`)) + + return { + status: StatusCodes.OK, + results: JSON.stringify({ + sessionKey, + userWallet: keyResult.userWallet, + userId: keyResult.userId, + hash: keyResult.hash + }) + } +} diff --git a/src/commands/app/listApiKeys.ts b/src/commands/app/listApiKeys.ts new file mode 100644 index 0000000..d68c119 --- /dev/null +++ b/src/commands/app/listApiKeys.ts @@ -0,0 +1,77 @@ +import { NvmAccount, NvmApp } from '@nevermined-io/sdk' +import { + StatusCodes, +} from '../../utils' +import chalk from 'chalk' +import { ExecutionOutput } from '../../models/ExecutionOutput' +import { Logger } from 'log4js' +import { ConfigEntry } from '../../models/ConfigDefinition' + + +export const listApiKeys = async ( + _nvmApp: NvmApp, + _publisherAccount: NvmAccount, + _argv: any, + config: ConfigEntry, + logger: Logger +): Promise => { + + const NVM_BACKEND_URL = config.nvm.neverminedBackendUri || undefined + if (!NVM_BACKEND_URL) { + return { + status: StatusCodes.ERROR, + errorMessage: `Missing Nevermined Backend URL` + } + } + + logger.info(chalk.dim(`Listing NVM API Keys ...`)) + // logger.info(config.nvm.marketplaceAuthToken) + // logger.info(config.nvm.marketplaceUri) + // const clientAssertion = await nvmApp.sdk.utils.jwt.generateClientAssertion(publisherAccount) + // const marketplaceAuthToken = await nvmApp.sdk.services.marketplace.login(clientAssertion) + + const options = { + method: 'GET', + headers: { + 'Accept': '*/*', + 'Authorization': `Bearer ${config.nvm.marketplaceAuthToken}` + } + } + + logger.debug(`${NVM_BACKEND_URL}/api/v1/api-keys`) + logger.debug(JSON.stringify(options)) + const _result = await fetch(`${NVM_BACKEND_URL}/api/v1/api-keys`, options) + + if (_result.status !== 200) { + logger.error(`Error listing NVM API Keys: ${_result.statusText}`) + logger.error(await _result.text()) + return { + status: StatusCodes.ERROR, + errorMessage: `Error listing NVM API Keys: ${_result.statusText}` + } + } + + const keyResult = await _result.json() + logger.info(chalk.green(`NVM API Keys found ${keyResult.totalResults}`)) + + + keyResult.apiKeys.forEach((key: any) => { + logger.info('---------------------------------') + logger.info(chalk.dim(`\tName: ${chalk.yellow(key.name)}`)) + if (key.isActive) + logger.info(chalk.dim(`\tIs Active?: ${chalk.green('yes')}`)) + else + logger.info(chalk.dim(`\tIs Active?: ${chalk.red('no')}`)) + + logger.info(chalk.dim(`\tHash: ${chalk.yellow(key.hash)}`)) + logger.info(chalk.dim(`\tWallet: ${chalk.yellow(key.userWallet)}`)) + logger.info(chalk.dim(`\tCreated/Expires: ${chalk.yellow(key.createdAt)} / ${chalk.yellow(key.expiresAt)}`)) + }) + + return { + status: StatusCodes.OK, + results: JSON.stringify({ + keyResult + }) + } +} diff --git a/src/commands/app/revokeApiKey.ts b/src/commands/app/revokeApiKey.ts new file mode 100644 index 0000000..9cbfc94 --- /dev/null +++ b/src/commands/app/revokeApiKey.ts @@ -0,0 +1,55 @@ +import { NvmAccount, NvmApp } from '@nevermined-io/sdk' +import { + StatusCodes, +} from '../../utils' +import chalk from 'chalk' +import { ExecutionOutput } from '../../models/ExecutionOutput' +import { Logger } from 'log4js' +import { ConfigEntry } from '../../models/ConfigDefinition' + + +export const revokeApiKey = async ( + _nvmApp: NvmApp, + _publisherAccount: NvmAccount, + argv: any, + config: ConfigEntry, + logger: Logger +): Promise => { + + const NVM_BACKEND_URL = config.nvm.neverminedBackendUri || undefined + if (!NVM_BACKEND_URL) { + return { + status: StatusCodes.ERROR, + errorMessage: `Missing Nevermined Backend URL` + } + } + + logger.info(chalk.dim(`Revoking API Keys ...`)) + + const options = { + method: 'DELETE', + headers: { + 'Accept': '*/*', + 'Authorization': `Bearer ${argv.hash}` + } + } + + logger.debug(`${NVM_BACKEND_URL}/api/v1/api-keys`) + logger.debug(JSON.stringify(options)) + const _result = await fetch(`${NVM_BACKEND_URL}/api/v1/api-keys`, options) + + if (_result.status !== 200 && _result.status !== 201) { + logger.error(`Error revoking NVM API Key: ${_result.statusText}`) + logger.error(await _result.text()) + return { + status: StatusCodes.ERROR, + errorMessage: `Error revoking NVM API Key: ${_result.statusText}` + } + } + + logger.info(chalk.green(`NVM API Key revoked`)) + + return { + status: StatusCodes.OK + } +} diff --git a/src/commands/index.ts b/src/commands/index.ts index 9c55ddd..31b1f54 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -67,3 +67,6 @@ export * from './app/orderPlan' export * from './app/balancePlan' export * from './app/getAccessToken' export * from './app/downloadFiles' +export * from './app/createApiKey' +export * from './app/listApiKeys' +export * from './app/revokeApiKey' diff --git a/src/models/ConfigDefinition.ts b/src/models/ConfigDefinition.ts index 6656545..65f8761 100644 --- a/src/models/ConfigDefinition.ts +++ b/src/models/ConfigDefinition.ts @@ -1,11 +1,11 @@ -import { NeverminedOptions, NvmAccount } from '@nevermined-io/sdk' +import { NvmAccount, NeverminedAppOptions } from '@nevermined-io/sdk' export interface CliConfig { [index: string]: ConfigEntry } export interface ConfigEntry { - nvm: NeverminedOptions + nvm: NeverminedAppOptions signer: NvmAccount envDescription?: string envUrl?: string diff --git a/src/utils/config.ts b/src/utils/config.ts index 5bc4e8d..a3f50aa 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -159,6 +159,7 @@ export async function getConfig( config.nvm.marketplaceUri = process.env.MARKETPLACE_API_URL config.nvm.graphHttpUri = process.env.GRAPH_URL if (process.env.NVM_NODE_URL) config.nvm.neverminedNodeUri = process.env.NVM_NODE_URL + if (process.env.NVM_BACKEND_URL) config.nvm.neverminedBackendUri = process.env.NVM_BACKEND_URL if (process.env.NODE_ADDRESS) config.nvm.neverminedNodeAddress = process.env.NODE_ADDRESS if (process.env.TOKEN_ADDRESS) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 2fc79e0..67a4f90 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -122,6 +122,11 @@ export const loginMarketplaceApi = async ( return true } +export const getNeverminedContractAbi = (contractName: string, networkName: string) => { + const abiPath = `${ARTIFACTS_PATH}/${contractName}.${networkName.toLowerCase()}.json` + return JSON.parse(fs.readFileSync(abiPath).toString()) +} + export const loadNeverminedConfigContract = async (nvm: Nevermined, config: ConfigEntry) => { const abiNvmConfig = `${ARTIFACTS_PATH}/NeverminedConfig.${config.networkName?.toLowerCase()}.json` const nvmConfigAbi = JSON.parse(fs.readFileSync(abiNvmConfig).toString())