diff --git a/packages/access-api/src/models/provisions.js b/packages/access-api/src/models/provisions.js index 7d3da0408..ab62340e5 100644 --- a/packages/access-api/src/models/provisions.js +++ b/packages/access-api/src/models/provisions.js @@ -1,28 +1,32 @@ /* eslint-disable no-void */ /** - * @typedef {import("../types/provisions").ProvisionsStorage} Provisions + * @template {import("@ucanto/interface").DID} ServiceId + * @typedef {import("../types/provisions").ProvisionsStorage} Provisions */ /** - * @param {Array} storage - * @returns {Provisions} + * @template {import("@ucanto/interface").DID} ServiceId + * @param {ServiceId} service + * @param {Array>} storage + * @returns {Provisions} */ -export function createProvisions(storage = []) { - /** @type {Provisions['hasStorageProvider']} */ +export function createProvisions(service, storage = []) { + /** @type {Provisions['hasStorageProvider']} */ const hasStorageProvider = async (consumerId) => { const hasRowWithSpace = storage.some(({ space }) => space === consumerId) return hasRowWithSpace } - /** @type {Provisions['put']} */ + /** @type {Provisions['put']} */ const put = async (item) => { storage.push(item) } - /** @type {Provisions['count']} */ + /** @type {Provisions['count']} */ const count = async () => { return BigInt(storage.length) } return { + service, count, put, hasStorageProvider, @@ -42,6 +46,7 @@ export function createProvisions(storage = []) { */ /** + * @template {import("@ucanto/interface").DID} ServiceId * Provisions backed by a kyseli database (e.g. sqlite or cloudflare d1) */ export class DbProvisions { @@ -49,17 +54,19 @@ export class DbProvisions { #db /** + * @param {ServiceId} service * @param {ProvisionsDatabase} db */ - constructor(db) { + constructor(service, db) { + this.service = service this.#db = db this.tableNames = { provisions: /** @type {const} */ ('provisions'), } - void (/** @type {Provisions} */ (this)) + void (/** @type {Provisions} */ (this)) } - /** @type {Provisions['count']} */ + /** @type {Provisions['count']} */ async count(...items) { const { size } = await this.#db .selectFrom(this.tableNames.provisions) @@ -68,7 +75,7 @@ export class DbProvisions { return BigInt(size) } - /** @type {Provisions['put']} */ + /** @type {Provisions['put']} */ async put(item) { /** @type {ProvisionsRow} */ const row = { @@ -132,7 +139,7 @@ export class DbProvisions { ) } - /** @type {Provisions['hasStorageProvider']} */ + /** @type {Provisions['hasStorageProvider']} */ async hasStorageProvider(consumerDid) { const { provisions } = this.tableNames const { size } = await this.#db diff --git a/packages/access-api/src/service/index.js b/packages/access-api/src/service/index.js index db5e469c7..cef74bbb0 100644 --- a/packages/access-api/src/service/index.js +++ b/packages/access-api/src/service/index.js @@ -234,9 +234,10 @@ export function service(ctx) { } /** + * @template {Ucanto.DID} Service * @param {Ucanto.DID<'key'>} space * @param {Spaces} spaces - * @param {import('../types/provisions.js').ProvisionsStorage} provisions + * @param {import('../types/provisions.js').ProvisionsStorage} provisions * @returns {Promise} */ async function spaceHasStorageProvider(space, spaces, provisions) { @@ -257,8 +258,9 @@ async function spaceHasStorageProviderFromVoucherRedeem(space, spaces) { } /** + * @template {Ucanto.DID} Service * @param {Ucanto.DID<'key'>} space - * @param {import('../types/provisions.js').ProvisionsStorage} provisions + * @param {import('../types/provisions.js').ProvisionsStorage} provisions * @returns {Promise} */ async function spaceHasStorageProviderFromProviderAdd(space, provisions) { diff --git a/packages/access-api/src/service/provider-add.js b/packages/access-api/src/service/provider-add.js index 99bfa1f42..7253be3d9 100644 --- a/packages/access-api/src/service/provider-add.js +++ b/packages/access-api/src/service/provider-add.js @@ -16,8 +16,9 @@ import * as validator from '@ucanto/validator' */ /** + * @template {Ucanto.DID} ServiceId * @param {object} options - * @param {import('../types/provisions').ProvisionsStorage} options.provisions + * @param {import('../types/provisions').ProvisionsStorage} options.provisions * @returns {ProviderAddHandler} */ export function createProviderAddHandler(options) { @@ -35,10 +36,14 @@ export function createProviderAddHandler(options) { message: 'Issuer must be a mailto DID', } } + if (provider !== options.provisions.service) { + throw new Error(`Provider must be ${options.provisions.service}`) + } await options.provisions.put({ invocation, space: consumer, - provider, + // eslint-disable-next-line object-shorthand + provider: /** @type {ServiceId} */ (provider), account: accountDID, }) return {} diff --git a/packages/access-api/src/types/provisions.ts b/packages/access-api/src/types/provisions.ts index a0ec3d134..f87620d4b 100644 --- a/packages/access-api/src/types/provisions.ts +++ b/packages/access-api/src/types/provisions.ts @@ -6,24 +6,25 @@ export type AlphaStorageProvider = 'did:web:web3.storage:providers:w3up-alpha' /** * action which results in provisionment of a space consuming a storage provider */ -export interface Provision { +export interface Provision> { invocation: Ucanto.Invocation space: Ucanto.DID<'key'> account: Ucanto.DID<'mailto'> - provider: AlphaStorageProvider + provider: AlphaStorageProvider | ServiceDID } /** * stores instances of a storage provider being consumed by a consumer */ -export interface ProvisionsStorage { +export interface ProvisionsStorage> { + service: ServiceDID hasStorageProvider: (consumer: Ucanto.DID<'key'>) => Promise /** * ensure item is stored * * @param item - provision to store */ - put: (item: Provision) => Promise + put: (item: Provision) => Promise /** * get number of stored items diff --git a/packages/access-api/src/utils/context.js b/packages/access-api/src/utils/context.js index 2b3636d89..c1769dd4c 100644 --- a/packages/access-api/src/utils/context.js +++ b/packages/access-api/src/utils/context.js @@ -62,9 +62,10 @@ export function getContext(request, env, ctx) { env: config.ENV, }) const url = new URL(request.url) + const signer = Signer.parse(config.PRIVATE_KEY).withDID(config.DID) return { log, - signer: Signer.parse(config.PRIVATE_KEY).withDID(config.DID), + signer, config, url, models: { @@ -78,7 +79,7 @@ export function getContext(request, env, ctx) { spaces: new Spaces(config.DB), validations: new Validations(config.VALIDATIONS), accounts: new Accounts(config.DB), - provisions: new DbProvisions(createD1Database(config.DB)), + provisions: new DbProvisions(signer.did(), createD1Database(config.DB)), }, email, uploadApi: createUploadApiConnection({ diff --git a/packages/access-api/test/helpers/types.ts b/packages/access-api/test/helpers/types.ts index 835346b62..5c2ba1e02 100644 --- a/packages/access-api/test/helpers/types.ts +++ b/packages/access-api/test/helpers/types.ts @@ -3,7 +3,7 @@ import type { Miniflare } from 'miniflare' export interface HelperTestContext { issuer: Ucanto.Signer> - service: Ucanto.Signer + service: Ucanto.Signer> conn: Ucanto.ConnectionView> mf: Miniflare } diff --git a/packages/access-api/test/provider-add.test.js b/packages/access-api/test/provider-add.test.js index b3071dcc9..81c20ae21 100644 --- a/packages/access-api/test/provider-add.test.js +++ b/packages/access-api/test/provider-add.test.js @@ -22,7 +22,10 @@ for (const providerAddHandlerVariant of /** @type {const} */ ([ name: 'handled by createProviderAddHandler', ...(() => { const spaceWithStorageProvider = principal.ed25519.generate() - const provisions = createProvisions() + const service = { + did: () => /** @type {const} */ ('did:web:web3.storage'), + } + const provisions = createProvisions(service.did()) return { spaceWithStorageProvider, provisions, @@ -47,7 +50,7 @@ for (const providerAddHandlerVariant of /** @type {const} */ ([ with: `did:mailto:example.com:foo`, nb: { consumer: space.did(), - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: providerAddHandlerVariant.provisions.service, }, }) .delegate() @@ -149,7 +152,7 @@ for (const accessApiVariant of /** @type {const} */ ([ can: 'provider/add', with: accountDid, nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: space.did(), }, }, @@ -218,7 +221,7 @@ for (const accessApiVariant of /** @type {const} */ ([ can: 'provider/add', with: accountDid, nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: space.did(), }, }, @@ -291,7 +294,7 @@ export function createEmail(storage) { * @param {Ucanto.Signer>} options.deviceA * @param {Ucanto.Signer>} options.space * @param {Ucanto.Principal>} options.accountA - * @param {Ucanto.Principal} options.service - web3.storage service + * @param {Ucanto.Principal>} options.service - web3.storage service * @param {import('miniflare').Miniflare} options.miniflare * @param {(invocation: Ucanto.Invocation) => Promise} options.invoke * @param {ValidationEmailSend[]} options.emails @@ -375,7 +378,7 @@ async function testAuthorizeClaimProviderAdd(options) { audience: service, with: accountA.did(), nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: space.did(), }, proofs: claimedDelegations, diff --git a/packages/access-api/test/provisions.test.js b/packages/access-api/test/provisions.test.js index 4e67ec851..c28b0142b 100644 --- a/packages/access-api/test/provisions.test.js +++ b/packages/access-api/test/provisions.test.js @@ -8,9 +8,9 @@ import { CID } from 'multiformats' describe('DbProvisions', () => { it('should persist provisions', async () => { - const { d1 } = await context() + const { d1, service } = await context() const db = createD1Database(d1) - const storage = new DbProvisions(db) + const storage = new DbProvisions(service.did(), db) const count = 2 + Math.round(Math.random() * 3) const spaceA = await principal.ed25519.generate() const [firstProvision, ...lastProvisions] = await Promise.all( @@ -28,7 +28,7 @@ describe('DbProvisions', () => { }, }) .delegate() - /** @type {import('../src/types/provisions.js').Provision} */ + /** @type {import('../src/types/provisions.js').Provision<'did:web:web3.storage:providers:w3up-alpha'>} */ const provision = { invocation, space: spaceA.did(), diff --git a/packages/capabilities/src/provider.js b/packages/capabilities/src/provider.js index e403cd74f..a29f9b634 100644 --- a/packages/capabilities/src/provider.js +++ b/packages/capabilities/src/provider.js @@ -11,8 +11,12 @@ import { capability, DID, literal, struct } from '@ucanto/validator' import { equalWith, fail, equal } from './utils.js' -export const StorageProvider = literal( - 'did:web:web3.storage:providers:w3up-alpha' +export const Web3StorageId = literal('did:web:web3.storage').or( + literal('did:web:staging.web3.storage') +) + +export const Provider = Web3StorageId.or(DID.match({ method: 'key' })).or( + DID.match({ method: 'web' }) ) export const AccountDID = DID.match({ method: 'mailto' }) @@ -24,7 +28,7 @@ export const add = capability({ can: 'provider/add', with: AccountDID, nb: struct({ - provider: StorageProvider, + provider: Provider, consumer: DID.match({ method: 'key' }), }), derives: (child, parent) => { diff --git a/packages/capabilities/test/capabilities/provider.test.js b/packages/capabilities/test/capabilities/provider.test.js index 677ccdcf4..b6918b31a 100644 --- a/packages/capabilities/test/capabilities/provider.test.js +++ b/packages/capabilities/test/capabilities/provider.test.js @@ -18,7 +18,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: space.did(), }, proofs: await createAuthorization({ agent, service, account }), @@ -35,7 +35,7 @@ describe('provider/add', function () { assert.deepEqual(result.audience.did(), service.did()) assert.equal(result.capability.can, 'provider/add') assert.deepEqual(result.capability.nb, { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: space.did(), }) } @@ -50,7 +50,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: space.did(), }, }) @@ -78,7 +78,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: space.did(), }, proofs: [delegation], @@ -107,7 +107,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: space.did(), }, proofs: [attestation], @@ -131,7 +131,7 @@ describe('provider/add', function () { with: bobAccount.did(), // @ts-expect-error nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), }, }) }, /Error: Invalid 'nb' - Object contains invalid field "consumer"/) @@ -145,7 +145,7 @@ describe('provider/add', function () { audience: service, with: bobAccount.did(), nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), // @ts-expect-error consumer: 'did:mailto:web3.storage:user', }, @@ -162,27 +162,26 @@ describe('provider/add', function () { with: bobAccount.did(), // @ts-expect-error - missing provider nb: { - // provider: 'did:web:web3.storage:providers:w3up-alpha', + // provider: service.did(), consumer: bob.did(), }, }) }, /Error: Invalid 'nb' - Object contains invalid field "provider"/) }) - it('requires nb.provider be registered', async function () { + it('does not require nb.provider be registered', async function () { const bobAccount = bob.withDID('did:mailto:bob.com:bob') - assert.throws(() => { - Provider.add.invoke({ + await Provider.add + .invoke({ issuer: bob, audience: service, with: bobAccount.did(), nb: { - // @ts-expect-error - not registered provider: 'did:web:web3.storage:providers:w3up-beta', consumer: bob.did(), }, }) - }, /Error: Invalid 'nb' - Object contains invalid field "provider"/) + .delegate() }) it('can delegate provider/add', async () => { @@ -194,7 +193,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: space.did(), }, proofs: [ @@ -203,7 +202,7 @@ describe('provider/add', function () { audience: bob, with: account, nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: space.did(), }, proofs: await createAuthorization({ agent, service, account }), @@ -229,7 +228,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: space.did(), }, proofs: [ @@ -238,7 +237,7 @@ describe('provider/add', function () { audience: bob, with: account, nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), }, proofs: await createAuthorization({ agent, service, account }), }), @@ -263,7 +262,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: space.did(), }, proofs: [ @@ -297,7 +296,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: bob.did(), }, proofs: [ @@ -332,7 +331,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: bob.did(), }, proofs: [ @@ -372,7 +371,7 @@ describe('provider/add', function () { audience: service, with: 'did:mailto:mallory.com:bob', nb: { - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), consumer: bob.did(), }, proofs: [ @@ -420,7 +419,7 @@ describe('provider/add', function () { with: account.did(), nb: { consumer: space.did(), - provider: 'did:web:web3.storage:providers:w3up-alpha', + provider: service.did(), }, // NOTE: no proofs! })