diff --git a/packages/access-api/src/bindings.d.ts b/packages/access-api/src/bindings.d.ts index f4a7781be..8b614a381 100644 --- a/packages/access-api/src/bindings.d.ts +++ b/packages/access-api/src/bindings.d.ts @@ -38,7 +38,7 @@ export interface Env { export interface RouteContext { log: Logging - signer: Signer + signer: Signer<'key'> config: ReturnType url: URL email: Email diff --git a/packages/access-api/src/kvs/accounts.js b/packages/access-api/src/kvs/accounts.js index 466385a92..6a925d650 100644 --- a/packages/access-api/src/kvs/accounts.js +++ b/packages/access-api/src/kvs/accounts.js @@ -31,7 +31,7 @@ export class Accounts { tableName: 'accounts', data: { did: capability.nb.account, - product: capability.nb.product, + product: capability.with, email: capability.nb.identity.replace('mailto:', ''), agent: invocation.issuer.did(), }, diff --git a/packages/access-api/src/service/voucher-claim.js b/packages/access-api/src/service/voucher-claim.js index 2859c3ce1..93ccdfa97 100644 --- a/packages/access-api/src/service/voucher-claim.js +++ b/packages/access-api/src/service/voucher-claim.js @@ -6,29 +6,43 @@ import { delegationToString } from '@web3-storage/access/encoding' * @param {import('../bindings').RouteContext} ctx */ export function voucherClaimProvider(ctx) { + // Provider should have access to delegated vouchers which it can + // redelegate. Currently however we just identify "free-tier" with a + // provider DID. + const products = new Map([ + [ + ctx.signer.did(), + Voucher.redeem.delegate({ + audience: ctx.signer, + issuer: ctx.signer, + expiration: Infinity, + with: ctx.signer.did(), + nb: { + identity: 'mailto:*', + }, + }), + ], + ]) + return Server.provide(Voucher.claim, async ({ capability, invocation }) => { - const proof = await Voucher.redeem.delegate({ - audience: ctx.signer, - issuer: ctx.signer, - expiration: Infinity, - with: ctx.signer.did(), - nb: { - product: 'product:*', - identity: 'mailto:*', - account: 'did:*', - }, - }) + const productID = capability.nb.product + const proof = await products.get(productID) + if (!proof) { + return new Server.Failure(`Product ${capability.nb.product} is not known`) + } const inv = await Voucher.redeem .invoke({ issuer: ctx.signer, audience: invocation.issuer, - with: ctx.signer.did(), + with: productID, lifetimeInSeconds: 60 * 10, // 10 mins nb: { + // currently we delegate back to the DID on whos behalf claim was + // issued. In the future will allow omitting this that voucher could + // be requested without specifying account it will be used on. account: capability.with, identity: capability.nb.identity, - product: capability.nb.product, }, proofs: [proof], }) diff --git a/packages/access-api/test/helpers/utils.js b/packages/access-api/test/helpers/utils.js index c73726c2c..a6319771c 100644 --- a/packages/access-api/test/helpers/utils.js +++ b/packages/access-api/test/helpers/utils.js @@ -37,8 +37,7 @@ export async function createAccount(issuer, service, conn, email) { nb: { // @ts-ignore identity: `mailto:${email}`, - product: 'product:free', - service: service.did(), + product: service.did(), }, proofs: [ await Any.any.delegate({ @@ -64,7 +63,6 @@ export async function createAccount(issuer, service, conn, email) { nb: { account: account.did(), identity: delegation.capabilities[0].nb.identity, - product: delegation.capabilities[0].nb.product, }, proofs: [ delegation, diff --git a/packages/access-api/test/voucher-claim.test.js b/packages/access-api/test/voucher-claim.test.js index ea5364c31..09dd4d9d7 100644 --- a/packages/access-api/test/voucher-claim.test.js +++ b/packages/access-api/test/voucher-claim.test.js @@ -17,8 +17,7 @@ test('should voucher/claim', async (t) => { with: issuer.did(), nb: { identity: 'mailto:email@dag.house', - product: 'product:free', - service: service.did(), + product: service.did(), }, }) .execute(conn) @@ -35,7 +34,7 @@ test('should voucher/claim', async (t) => { t.deepEqual(delegation.issuer.did(), service.did()) t.deepEqual(delegation.audience.did(), issuer.did()) t.deepEqual(delegation.capabilities[0].nb.account, issuer.did()) - t.deepEqual(delegation.capabilities[0].nb.product, 'product:free') + t.deepEqual(delegation.capabilities[0].with, service.did()) t.deepEqual(delegation.capabilities[0].nb.identity, 'mailto:email@dag.house') if (Delegation.isDelegation(delegation.proofs[0])) { @@ -45,9 +44,7 @@ test('should voucher/claim', async (t) => { with: service.did(), can: 'voucher/redeem', nb: { - account: 'did:*', identity: 'mailto:*', - product: 'product:*', }, }, ]) diff --git a/packages/access-api/test/voucher-redeem.test.js b/packages/access-api/test/voucher-redeem.test.js index 1ad8e9bc8..381d5120f 100644 --- a/packages/access-api/test/voucher-redeem.test.js +++ b/packages/access-api/test/voucher-redeem.test.js @@ -5,14 +5,13 @@ import { stringToDelegation } from '@web3-storage/access/encoding' import { StoreMemory } from '@web3-storage/access/stores/store-memory' import { context, test } from './helpers/context.js' import { createAccount } from './helpers/utils.js' -import { Accounts } from '../src/kvs/accounts.js' -test.beforeEach(async (t) => { +test.before(async (t) => { t.context = await context() }) test('should return account/redeem', async (t) => { - const { issuer, service, conn, mf, db } = t.context + const { issuer, service, conn, mf } = t.context const store = new StoreMemory() const account = await store.createAccount() @@ -23,8 +22,7 @@ test('should return account/redeem', async (t) => { with: account.did(), nb: { identity: 'mailto:email@dag.house', - product: 'product:free', - service: service.did(), + product: service.did(), }, proofs: [ await Any.any.delegate({ @@ -54,7 +52,6 @@ test('should return account/redeem', async (t) => { nb: { account: account.did(), identity: delegation.capabilities[0].nb.identity, - product: delegation.capabilities[0].nb.product, }, proofs: [ delegation, @@ -73,24 +70,22 @@ test('should return account/redeem', async (t) => { return t.fail() } - const accounts = new Accounts(await mf.getKVNamespace('ACCOUNTS'), db) - - // check db for account - t.like(await accounts.get(account.did()), { - did: account.did(), - product: 'product:free', - email: 'email@dag.house', - agent: issuer.did(), - }) - - // check account delegations - const delegations = await accounts.getDelegations('mailto:email@dag.house') + const accounts = await mf.getKVNamespace('ACCOUNTS') - if (!delegations) { + const delEncoded = /** @type {string[]|undefined} */ ( + await accounts.get('mailto:email@dag.house', { + type: 'json', + }) + ) + if (!delEncoded) { return t.fail('no delegation for email') } - const del = await stringToDelegation(delegations[0]) + const del = await stringToDelegation( + /** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/access/capabilities/types').Any]>} */ ( + delEncoded[0] + ) + ) t.deepEqual(del.audience.did(), service.did()) t.deepEqual(del.capabilities[0].can, '*') @@ -127,30 +122,3 @@ test('should save multiple account delegation', async (t) => { // @ts-ignore t.assert(delEncoded.length === 2) }) - -test('should fail with wrong resource', async (t) => { - const { issuer, service, conn } = t.context - - const redeem = await Voucher.redeem - .invoke({ - issuer, - audience: service, - with: issuer.did(), - nb: { - account: issuer.did(), - identity: 'mailto:email@dag.house', - product: 'product:free', - }, - }) - .execute(conn) - - if (redeem.error) { - t.true(redeem.error) - t.deepEqual( - redeem.message, - `Resource ${issuer.did()} does not service did ${service.did()}` - ) - } else { - t.fail('should fail') - } -}) diff --git a/packages/access-client/src/agent.js b/packages/access-client/src/agent.js index ed359eb57..eb14a1ff8 100644 --- a/packages/access-client/src/agent.js +++ b/packages/access-client/src/agent.js @@ -59,7 +59,7 @@ export async function connection(principal, _fetch, url) { } /** - * @template {Ucanto.Signer} T + * @template {Ucanto.Signer<"key">} T * Agent */ export class Agent { @@ -85,7 +85,7 @@ export class Agent { } /** - * @template {Ucanto.Signer} T + * @template {Ucanto.Signer<"key">} T * @param {AgentCreateOptions} opts */ static async create(opts) { @@ -149,8 +149,7 @@ export class Agent { with: account.did(), nb: { identity: URI.from(`mailto:${email}`), - product: 'product:free', - service: service.did(), + product: service.did(), }, proofs: [delegationToAgent], }) @@ -176,7 +175,6 @@ export class Agent { nb: { account: account.did(), identity: voucherRedeem.capabilities[0].nb.identity, - product: voucherRedeem.capabilities[0].nb.product, }, proofs: [voucherRedeem, delegationToService], }) diff --git a/packages/access-client/src/capabilities/voucher.js b/packages/access-client/src/capabilities/voucher.js index 26d668cd9..bbac4ea77 100644 --- a/packages/access-client/src/capabilities/voucher.js +++ b/packages/access-client/src/capabilities/voucher.js @@ -3,9 +3,9 @@ import { equalWith, equal, fail } from './utils.js' import { any } from './any.js' /** - * Products are identified by the CID of the DAG that describes them. + * Products are identified by a did:key identifier. */ -export const Product = URI.uri() +export const Product = DID.match({ method: 'key' }) /** * Verifiable identity to whom voucher is issued. Currently it is a `mailto:` @@ -17,6 +17,10 @@ export const Identity = URI.match({ protocol: 'mailto:' }) * Services are identified using did:key identifier. */ export const Service = DID.match({ method: 'key' }) +/** + * Spaces are identified using did:key identifier. + */ +export const Account = DID.match({ method: 'key' }) /** * Capability can only be delegated (but not invoked) allowing audience to @@ -39,37 +43,30 @@ const base = any.or(voucher) /** * Capability can be invoked by an agent to claim a voucher for a specific - * user identifier (currently email address). - * - * The agent MAY issue claim with own DID or a DID it is delegate of. If `with` - * is different from `iss`, it is implied that the voucher is claimed for the - * DID in the `with` field. If `with` is same as `iss` it is implies that - * voucher is claimed for an unspecified `did`. + * product (identified by `nb.product` DID) for a verifiable identifier + * (currently email address). */ export const claim = base.derive({ to: capability({ can: 'voucher/claim', - with: URI.match({ protocol: 'did:' }), + with: Account, nb: { /** - * URI of the product agent is requesting a voucher of. + * DID of the product agent is requesting a voucher/redeem for. */ product: Product, /** * Verifiable identity on who's behalf behalf claim is made. */ + // Once we roll out DKIM based system we could consider just + // using did:mailto: in with field. identity: Identity, - /** - * Optional service DID who's voucher is been requested. - */ - service: Service.optional(), }, derives: (child, parent) => { return ( fail(equalWith(child, parent)) || fail(equal(child.nb.product, parent.nb.product, 'product')) || fail(equal(child.nb.identity, parent.nb.identity, 'identity')) || - fail(equal(child.nb.service, parent.nb.service, 'service')) || true ) }, @@ -84,29 +81,27 @@ export const claim = base.derive({ export const redeem = voucher.derive({ to: capability({ can: 'voucher/redeem', - with: URI.match({ protocol: 'did:' }), + /** + * DID of the product which can be installed into a space by invoking + * this capability. + */ + with: Product, nb: { - /** - * Link of the product voucher is for. Must be the same as `nb.product` - * of `voucher/claim` that requested this. - */ - product: Product, /** * Verifiable identity to whom voucher is issued. It is a `mailto:` URL - * where this delegation is typically sent. + * where this delegation was sent. */ identity: Identity, /** * Space identifier where voucher can be redeemed. When service delegates - * `voucher/redeem` to the user agent it may omit this field to allow + * `vourche/redeem` to the user agent it may omit this field to allow * account to choose account. */ - account: URI.match({ protocol: 'did:' }), + account: Account, }, derives: (child, parent) => { return ( fail(equalWith(child, parent)) || - fail(equal(child.nb.product, parent.nb.product, 'product')) || fail(equal(child.nb.identity, parent.nb.identity, 'identity')) || fail(equal(child.nb.account, parent.nb.account, 'account')) || true diff --git a/packages/access-client/test/capabilities/voucher.test.js b/packages/access-client/test/capabilities/voucher.test.js index 2e55dbc26..8262445fb 100644 --- a/packages/access-client/test/capabilities/voucher.test.js +++ b/packages/access-client/test/capabilities/voucher.test.js @@ -3,28 +3,33 @@ import { access } from '@ucanto/validator' import { Verifier } from '@ucanto/principal/ed25519' import { delegate } from '@ucanto/core' import * as Voucher from '../../src/capabilities/voucher.js' -import { alice, bob, service, mallory } from '../helpers/fixtures.js' +import { + alice, + bob, + service as w3, + mallory as space, +} from '../helpers/fixtures.js' + +const product = `did:key:zFreeTier` describe('voucher capabilities', function () { it('should delegate from * to claim', async function () { - const account = mallory const claim = Voucher.claim.invoke({ issuer: alice, - audience: service, - with: account.did(), + audience: w3, + with: space.did(), nb: { identity: 'mailto:alice@email.com', - product: 'product:free', - service: service.did(), + product, }, proofs: [ await delegate({ - issuer: account, + issuer: space, audience: alice, capabilities: [ { can: 'voucher/*', - with: account.did(), + with: space.did(), }, ], }), @@ -36,12 +41,11 @@ describe('voucher capabilities', function () { principal: Verifier, }) if (!result.error) { - assert.deepEqual(result.audience.did(), service.did()) + assert.deepEqual(result.audience.did(), w3.did()) assert.equal(result.capability.can, 'voucher/claim') assert.deepEqual(result.capability.nb, { identity: 'mailto:alice@email.com', - product: 'product:free', - service: service.did(), + product, }) } }) @@ -49,12 +53,11 @@ describe('voucher capabilities', function () { it('should delegate from claim to claim', async function () { const claim = Voucher.claim.invoke({ issuer: bob, - audience: service, + audience: w3, with: alice.did(), nb: { identity: 'mailto:alice@email.com', - product: 'product:free', - service: service.did(), + product, }, proofs: [ await Voucher.claim.delegate({ @@ -63,8 +66,7 @@ describe('voucher capabilities', function () { with: alice.did(), nb: { identity: 'mailto:alice@email.com', - product: 'product:free', - service: service.did(), + product, }, }), ], @@ -76,12 +78,11 @@ describe('voucher capabilities', function () { }) if (!result.error) { - assert.deepEqual(result.audience.did(), service.did()) + assert.deepEqual(result.audience.did(), w3.did()) assert.equal(result.capability.can, 'voucher/claim') assert.deepEqual(result.capability.nb, { identity: 'mailto:alice@email.com', - product: 'product:free', - service: service.did(), + product, }) } else { assert.fail('should not error') @@ -91,12 +92,11 @@ describe('voucher capabilities', function () { it('should error claim to claim when caveats are different', async function () { const claim = Voucher.claim.invoke({ issuer: bob, - audience: service, + audience: w3, with: alice.did(), nb: { identity: 'mailto:alice@email.com', - product: 'product:freess', - service: service.did(), + product: 'did:key:freess', }, proofs: [ await Voucher.claim.delegate({ @@ -105,8 +105,7 @@ describe('voucher capabilities', function () { with: alice.did(), nb: { identity: 'mailto:alice@email.com', - product: 'product:free', - service: service.did(), + product: 'did:key:free', }, }), ],