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