diff --git a/README.md b/README.md index 9604f7b..cf6de84 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ w3 up recipies.txt - [`w3 proof ls`](#w3-proof-ls) - Key management - [`w3 key create`](#w3-key-create) +- UCAN-HTTP Bridge + - [`w3 bridge generate-tokens`](#w3-bridge-generate-tokens) - Advanced usage - [`w3 can space info`](#w3-can-space-info-did) coming soon! - [`w3 can space recover`](#w3-can-space-recover-email) coming soon! @@ -184,6 +186,12 @@ Print a new key pair. Does not change your current signing key - `--json` Export as dag-json +### `w3 bridge generate-tokens` + +Generate tokens that can be used as the `X-Auth-Secret` and `Authorization` headers required to use the UCAN-HTTP bridge. + +TODO: link to UCAN-HTTP bridge specification once it lands + ### `w3 can space info ` ### `w3 can space recover ` diff --git a/bin.js b/bin.js index 97867b0..915bf6c 100755 --- a/bin.js +++ b/bin.js @@ -8,6 +8,7 @@ import { Account, Space, Coupon, + Bridge, accessClaim, addSpace, listSpaces, @@ -179,6 +180,21 @@ cli ) .action(Coupon.issue) + cli + .command('bridge generate-tokens ') + .option('-c, --can', 'One or more abilities to delegate.') + .option( + '-e, --expiration', + 'Unix timestamp when the delegation is no longer valid. Zero indicates no expiration.', + 0 + ) + .option( + '-o, --output', + 'Path of file to write the exported delegation data to.' + ) + .action(Bridge.generateTokens) + + cli .command('delegation create ') .describe( diff --git a/bridge.js b/bridge.js new file mode 100644 index 0000000..e0ada97 --- /dev/null +++ b/bridge.js @@ -0,0 +1,56 @@ +import * as DID from '@ipld/dag-ucan/did' +import * as Account from './account.js' +import * as Space from './space.js' +import { getClient } from './lib.js' +import * as ucanto from '@ucanto/core' +import { base64url } from 'multiformats/bases/base64' +import cryptoRandomString from 'crypto-random-string'; + +export { Account, Space } + +/** + * @typedef {object} BridgeGenerateTokensOptions + * @property {string} resource + * @property {string[]|string} [can] + * @property {number} [expiration] + * + * @param {string} resource + * @param {BridgeGenerateTokensOptions} options + */ +export const generateTokens = async ( + resource, + { can = ['store/add', 'upload/add'], expiration } +) => { + const client = await getClient() + + const resourceDID = DID.parse(resource) + const abilities = can ? [can].flat() : [] + if (!abilities.length) { + console.error('Error: missing capabilities for delegation') + process.exit(1) + } + + const capabilities = /** @type {ucanto.API.Capabilities} */ ( + abilities.map((can) => ({ can, with: resourceDID.did() })) + ) + + const password = cryptoRandomString({ length: 32 }) + + const coupon = await client.coupon.issue({ + capabilities, + expiration: expiration === 0 ? Infinity : expiration, + password, + }) + + const { ok: bytes, error } = await coupon.archive() + if (!bytes) { + console.error(error) + return process.exit(1) + } + + console.log(` +X-Auth-Secret header: ${base64url.encode(new TextEncoder().encode(password))} + +Authorization header: ${base64url.encode(bytes)} +`) +} diff --git a/index.js b/index.js index f80e1b5..82e6fba 100644 --- a/index.js +++ b/index.js @@ -27,6 +27,7 @@ import * as ucanto from '@ucanto/core' import { ed25519 } from '@ucanto/principal' import chalk from 'chalk' export * as Coupon from './coupon.js' +export * as Bridge from './bridge.js' export { Account, Space } import ago from 's-ago' diff --git a/test/bin.spec.js b/test/bin.spec.js index 7c7ee4d..c60eb18 100644 --- a/test/bin.spec.js +++ b/test/bin.spec.js @@ -558,9 +558,9 @@ export const testSpace = { ) const infoWithProviderJson = await w3 - .args(['space', 'info', '--json']) - .env(context.env.alice) - .join() + .args(['space', 'info', '--json']) + .env(context.env.alice) + .join() assert.deepEqual(JSON.parse(infoWithProviderJson.output), { did: spaceDID, @@ -1272,6 +1272,15 @@ export const testKey = { }), } +export const testBridge = { + 'w3 bridge generate-tokens': test(async (assert, context) => { + const spaceDID = await loginAndCreateSpace(context) + const res = await w3.args(['bridge', 'generate-tokens', spaceDID]).join() + assert.match(res.output, /X-Auth-Secret header: u/) + assert.match(res.output, /Authorization header: u/) + }), +} + /** * @param {Test.Context} context * @param {object} options