Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Update voucher API #163

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/access-api/src/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface Env {

export interface RouteContext {
log: Logging
signer: Signer
signer: Signer<'key'>
config: ReturnType<typeof loadConfig>
url: URL
email: Email
Expand Down
2 changes: 1 addition & 1 deletion packages/access-api/src/kvs/accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
},
Expand Down
40 changes: 27 additions & 13 deletions packages/access-api/src/service/voucher-claim.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we create a separate DID for this? This is the DID of the service no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice I think we should have DID per product, that said I was not sure what the best way to thread that through, so I thought this was reasonable compromise to get at least discussion started.

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],
})
Expand Down
4 changes: 1 addition & 3 deletions packages/access-api/test/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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,
Expand Down
7 changes: 2 additions & 5 deletions packages/access-api/test/voucher-claim.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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])) {
Expand All @@ -45,9 +44,7 @@ test('should voucher/claim', async (t) => {
with: service.did(),
can: 'voucher/redeem',
nb: {
account: 'did:*',
identity: 'mailto:*',
product: 'product:*',
},
},
])
Expand Down
62 changes: 15 additions & 47 deletions packages/access-api/test/voucher-redeem.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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({
Expand Down Expand Up @@ -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,
Expand All @@ -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, '*')
Expand Down Expand Up @@ -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')
}
})
8 changes: 3 additions & 5 deletions packages/access-client/src/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export async function connection(principal, _fetch, url) {
}

/**
* @template {Ucanto.Signer} T
* @template {Ucanto.Signer<"key">} T
* Agent
*/
export class Agent {
Expand All @@ -85,7 +85,7 @@ export class Agent {
}

/**
* @template {Ucanto.Signer} T
* @template {Ucanto.Signer<"key">} T
* @param {AgentCreateOptions<T>} opts
*/
static async create(opts) {
Expand Down Expand Up @@ -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],
})
Expand All @@ -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],
})
Expand Down
45 changes: 20 additions & 25 deletions packages/access-client/src/capabilities/voucher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:`
Expand All @@ -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
Expand All @@ -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
)
},
Expand All @@ -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
Expand Down
Loading