From f328537eb6680d75f73a233ad7e907b545dba653 Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Wed, 17 Apr 2024 16:21:18 -0700 Subject: [PATCH] feat: add ability to generate delegations from spaces We needed to generate some delegations from the nft storage spaces to the nft storage agent principal. This patch adds a `--use-space-recovery-key` option to the `delegation create` command. This option must be passed a "recovery key" generated during the space creation process. When used, `delegation create` will only be able to generate delegations that use the space as both the issuer and resource of all specified capabilities. For example: ``` w3 delegation create --issuer-recovery-key "hamburger fiction this is not a real recovery key juice champion" did:key:z6MkkSc... --can 'upload/add' --can 'store/add' --can 'upload/get' --can 'filecoin/info' --base64 mAYIEALMc.... ``` I'm not sure this is the right design - particularly interested in @gozala's take on [link to line] TODO - [] align on design - [] write docs --- bin.js | 4 ++++ index.js | 43 ++++++++++++++++++++++++++++++------------- lib.js | 19 +++++++++++++++---- 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/bin.js b/bin.js index 05166d2..27a5df4 100755 --- a/bin.js +++ b/bin.js @@ -222,6 +222,10 @@ cli '--base64', 'Format as base64 identity CID string. Useful when saving it as an environment variable.' ) + .option( + '--use-space-recovery-key', + 'The recovery key of the space to use as the issuer of this delegation. Can only be used to grant capabilities on the given space.' + ) .action(createDelegation) cli diff --git a/index.js b/index.js index 82e6fba..0e986ad 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,8 @@ import { CarWriter } from '@ipld/car' import { filesFromPaths } from 'files-from-path' import * as Account from './account.js' import { spaceAccess } from '@web3-storage/w3up-client/capability/access' -import { AgentData } from '@web3-storage/access' +import * as W3Space from '@web3-storage/w3up-client/space' +import { AgentData, StoreMemory } from '@web3-storage/access' import * as Space from './space.js' import { getClient, @@ -30,6 +31,7 @@ export * as Coupon from './coupon.js' export * as Bridge from './bridge.js' export { Account, Space } import ago from 's-ago' +import { accountAccess } from '@web3-storage/access/access' /** * @@ -333,17 +335,33 @@ Providers: ${providers || chalk.dim('none')} /** * @param {string} audienceDID - * @param {object} opts - * @param {string[]|string} opts.can - * @param {string} [opts.name] - * @param {string} [opts.type] - * @param {number} [opts.expiration] - * @param {string} [opts.output] - * @param {string} [opts.with] - * @param {boolean} [opts.base64] + * @param {{ + * can: string | string[], + * name?: string, + * type?: string, + * expiration?: number, + * output?: string, + * with?: string, + * base64?: boolean, + * 'use-space-recovery-key'?: string + * }} opts */ export async function createDelegation(audienceDID, opts) { - const client = await getClient() + let client + const recoveryKey = opts['use-space-recovery-key'] + if (recoveryKey) { + const space = await W3Space.fromMnemonic(recoveryKey, { name: '' }) + + // create a client with an in-memory store and the space ID as the principal + client = await getClient({ principal: ed25519.format(space.signer), store: new StoreMemory() }) + + // "add" the space so that we don't get an error when trying to create a delegation later + // TODO: should we update the client so that this is unnecessary? maybe the client should warn but continue + // since it is possible to create a delegation from the root with no proofs? + await client.addSpace(await space.createAuthorization(client.agent, { access: accountAccess })) + } else { + client = await getClient() + } if (client.currentSpace() == null) { throw new Error('no current space, use `w3 space register` to create one.') @@ -453,7 +471,7 @@ export async function revokeDelegation(delegationCid, opts) { process.exit(1) } const result = await client.revokeDelegation( - /** @type {import('@ucanto/interface').UCANLink} */ (cid), + /** @type {import('@ucanto/interface').UCANLink} */(cid), { proofs: proof ? [proof] : [] } ) if (result.ok) { @@ -532,8 +550,7 @@ export async function listProofs(opts) { } console.log( chalk.dim( - `# ${proofs.length} proof${ - proofs.length === 1 ? '' : 's' + `# ${proofs.length} proof${proofs.length === 1 ? '' : 's' } for ${client.agent.did()}` ) ) diff --git a/lib.js b/lib.js index 25b2a0b..8c14dfa 100644 --- a/lib.js +++ b/lib.js @@ -57,16 +57,27 @@ export function filesizeMB(bytes) { return `${(bytes / 1000 / 1000).toFixed(1)}MB` } -/** Get a configured w3up store used by the CLI. */ +/** + * @typedef {import('@web3-storage/access/drivers/types').Driver} Store + */ + +/** + * Get a configured w3up store used by the CLI. + * @returns {Store} + */ export function getStore() { return new StoreConf({ profile: process.env.W3_STORE_NAME ?? 'w3cli' }) } /** * Get a new API client configured from env vars. + * @param {{ + * principal?: string + * store?: Store + * }} options */ -export function getClient() { - const store = getStore() +export function getClient(options = {}) { + const store = options.store || getStore() if (process.env.W3_ACCESS_SERVICE_URL || process.env.W3_UPLOAD_SERVICE_URL) { console.warn( @@ -132,7 +143,7 @@ export function getClient() { /** @type {import('@web3-storage/w3up-client/types').ClientFactoryOptions} */ const createConfig = { store, serviceConf } - const principal = process.env.W3_PRINCIPAL + const principal = options.principal ?? process.env.W3_PRINCIPAL if (principal) { createConfig.principal = Signer.parse(principal) }