diff --git a/packages/access-api/package.json b/packages/access-api/package.json index 18f23566c..34053dc01 100644 --- a/packages/access-api/package.json +++ b/packages/access-api/package.json @@ -94,6 +94,7 @@ "error", { "definedTypes": [ + "AsyncIterable", "AsyncIterableIterator", "Awaited", "D1Database", diff --git a/packages/access-api/src/routes/validate-email.js b/packages/access-api/src/routes/validate-email.js index ba4f003b4..ea3d8b199 100644 --- a/packages/access-api/src/routes/validate-email.js +++ b/packages/access-api/src/routes/validate-email.js @@ -1,6 +1,6 @@ import { - stringToDelegation, delegationsToString, + stringToDelegation, } from '@web3-storage/access/encoding' import * as Access from '@web3-storage/capabilities/access' import QRCode from 'qrcode' @@ -11,10 +11,12 @@ import { ValidateEmailError, PendingValidateEmail, } from '../utils/html.js' -import * as ucanto from '@ucanto/core' import * as validator from '@ucanto/validator' -import { Verifier, Absentee } from '@ucanto/principal' -import { collect } from 'streaming-iterables' +import { Verifier } from '@ucanto/principal' +import * as delegationsResponse from '../utils/delegations-response.js' +import * as accessConfirm from '../service/access-confirm.js' +import { provide } from '@ucanto/server' +import * as Ucanto from '@ucanto/interface' /** * @param {import('@web3-storage/worker-utils/router').ParsedRequest} req @@ -149,62 +151,35 @@ async function authorize(req, env) { }) if (confirmation.error) { - throw new Error(`unable to validate access session: ${confirmation}`) - } - if (confirmation.capability.with !== env.signer.did()) { - throw new Error(`Not a valid access/confirm delegation`) + throw new Error(`unable to validate access session: ${confirmation}`, { + cause: confirmation.error, + }) } - // Create a absentee signer for the account that authorized the delegation - const account = Absentee.from({ id: confirmation.capability.nb.iss }) - const agent = Verifier.parse(confirmation.capability.nb.aud) - - // It the future we should instead render a page and allow a user to select - // which delegations they wish to re-delegate. Right now we just re-delegate - // everything that was requested for all of the resources. - const capabilities = - /** @type {ucanto.UCAN.Capabilities} */ - ( - confirmation.capability.nb.att.map(({ can }) => ({ - can, - with: /** @type {ucanto.UCAN.Resource} */ ('ucan:*'), - })) - ) - - // create an delegation on behalf of the account with an absent signature. - const delegation = await ucanto.delegate({ - issuer: account, - audience: agent, - capabilities, - expiration: Infinity, - // We include all the delegations to the account so that the agent will - // have delegation chains to all the delegated resources. - // We should actually filter out only delegations that support delegated - // capabilities, but for now we just include all of them since we only - // implement sudo access anyway. - proofs: await collect( - env.models.delegations.find({ - audience: account.did(), - }) - ), - }) - - const attestation = await Access.session.delegate({ - issuer: env.signer, - audience: agent, - with: env.signer.did(), - nb: { proof: delegation.cid }, - expiration: Infinity, + const confirm = provide( + Access.confirm, + async ({ capability, invocation }) => { + return accessConfirm.handleAccessConfirm( + /** @type {Ucanto.Invocation} */ ( + invocation + ), + env + ) + } + ) + const confirmResult = await confirm(request, { + id: env.signer.verifier, + principal: Verifier, }) - - // Store the delegations so that they can be pulled with access/claim - // The fact that we're storing proofs chains that we pulled from the - // database is not great, but it's a tradeoff we're making for now. - await env.models.delegations.putMany(delegation, attestation) - - const authorization = delegationsToString([delegation, attestation]) - // Send delegations to the client through a websocket - await env.models.validations.putSession(authorization, agent.did()) + if (confirmResult.error) { + throw new Error('error confirming', { + cause: confirmResult.error, + }) + } + const { account, agent } = accessConfirm.parse(request) + const confirmDelegations = [ + ...delegationsResponse.decode(confirmResult.delegations), + ] // We render HTML page explaining to the user what has happened and providing // a QR code in the details if they want to drill down. @@ -213,20 +188,7 @@ async function authorize(req, env) { { - if (/too big to be stored in a qr/i.test(error.message)) { - env.log.error(error) - // It's not important to have the QR code - // eslint-disable-next-line unicorn/no-useless-undefined - return undefined - } - throw error - })} + ucan={delegationsToString(confirmDelegations)} /> ) ) diff --git a/packages/access-api/src/service/access-confirm.js b/packages/access-api/src/service/access-confirm.js new file mode 100644 index 000000000..9b04088b1 --- /dev/null +++ b/packages/access-api/src/service/access-confirm.js @@ -0,0 +1,121 @@ +import * as Ucanto from '@ucanto/interface' +import * as ucanto from '@ucanto/core' +import { Absentee } from '@ucanto/principal' +import { collect } from 'streaming-iterables' +import * as Access from '@web3-storage/capabilities/access' +import { delegationsToString } from '@web3-storage/access/encoding' +import * as delegationsResponse from '../utils/delegations-response.js' +import * as validator from '@ucanto/validator' + +/** + * @typedef {import('@web3-storage/capabilities/types').AccessConfirmSuccess} AccessConfirmSuccess + * @typedef {import('@web3-storage/capabilities/types').AccessConfirmFailure} AccessConfirmFailure + */ + +/** + * @param {Ucanto.Invocation} invocation + */ +export function parse(invocation) { + const capability = invocation.capabilities[0] + // Create a absentee signer for the account that authorized the delegation + const account = Absentee.from({ id: capability.nb.iss }) + const agent = { + did: () => validator.DID.match({ method: 'key' }).from(capability.nb.aud), + } + return { + account, + agent, + } +} + +/** + * @param {Ucanto.Invocation} invocation + * @param {import('../bindings').RouteContext} ctx + * @returns {Promise>} + */ +export async function handleAccessConfirm(invocation, ctx) { + const capability = invocation.capabilities[0] + if (capability.with !== ctx.signer.did()) { + throw new Error(`Not a valid access/confirm delegation`) + } + + const { account, agent } = parse(invocation) + + // It the future we should instead render a page and allow a user to select + // which delegations they wish to re-delegate. Right now we just re-delegate + // everything that was requested for all of the resources. + const capabilities = + /** @type {ucanto.UCAN.Capabilities} */ + ( + capability.nb.att.map(({ can }) => ({ + can, + with: /** @type {ucanto.UCAN.Resource} */ ('ucan:*'), + })) + ) + + const [delegation, attestation] = await createSessionProofs( + ctx.signer, + account, + agent, + capabilities, + // We include all the delegations to the account so that the agent will + // have delegation chains to all the delegated resources. + // We should actually filter out only delegations that support delegated + // capabilities, but for now we just include all of them since we only + // implement sudo access anyway. + ctx.models.delegations.find({ + audience: account.did(), + }), + Infinity + ) + + // Store the delegations so that they can be pulled with access/claim + // The fact that we're storing proofs chains that we pulled from the + // database is not great, but it's a tradeoff we're making for now. + await ctx.models.delegations.putMany(delegation, attestation) + + const authorization = delegationsToString([delegation, attestation]) + // Save delegations for the validation process + await ctx.models.validations.putSession(authorization, agent.did()) + + return { + delegations: delegationsResponse.encode([delegation, attestation]), + } +} + +/** + * @param {Ucanto.Signer} service + * @param {Ucanto.Principal>} account + * @param {Ucanto.Principal>} agent + * @param {Ucanto.Capabilities} capabilities + * @param {AsyncIterable} delegationProofs + * @param {number} expiration + * @returns {Promise<[delegation: Ucanto.Delegation, attestation: Ucanto.Delegation]>} + */ +export async function createSessionProofs( + service, + account, + agent, + capabilities, + delegationProofs, + expiration +) { + // create an delegation on behalf of the account with an absent signature. + const delegation = await ucanto.delegate({ + issuer: Absentee.from({ id: account.did() }), + audience: agent, + capabilities, + expiration, + proofs: [...(await collect(delegationProofs))], + }) + + const attestation = await Access.session.delegate({ + issuer: service, + audience: agent, + with: service.did(), + nb: { proof: delegation.cid }, + expiration, + }) + + return [delegation, attestation] +} diff --git a/packages/access-api/src/service/index.js b/packages/access-api/src/service/index.js index cef74bbb0..4ecb848cc 100644 --- a/packages/access-api/src/service/index.js +++ b/packages/access-api/src/service/index.js @@ -4,6 +4,7 @@ import * as Server from '@ucanto/server' import * as validator from '@ucanto/validator' import { Failure } from '@ucanto/server' import * as Space from '@web3-storage/capabilities/space' +import * as Access from '@web3-storage/capabilities/access' import { top } from '@web3-storage/capabilities/top' import { delegationToString, @@ -17,6 +18,7 @@ import { accessDelegateProvider } from './access-delegate.js' import { accessClaimProvider } from './access-claim.js' import { providerAddProvider } from './provider-add.js' import { Spaces } from '../models/spaces.js' +import { handleAccessConfirm } from './access-confirm.js' /** * @param {import('../bindings').RouteContext} ctx @@ -45,6 +47,21 @@ export function service(ctx) { config: ctx.config, })(...args) }, + confirm: Server.provide( + Access.confirm, + async ({ capability, invocation }) => { + // only needed in tests + if (ctx.config.ENV !== 'test') { + throw new Error(`access/confirm is disabled`) + } + return handleAccessConfirm( + /** @type {Ucanto.Invocation} */ ( + invocation + ), + ctx + ) + } + ), delegate: (...args) => { // disable until hardened in test/staging if (ctx.config.ENV === 'production') { diff --git a/packages/access-api/src/types/ucanto.ts b/packages/access-api/src/types/ucanto.ts new file mode 100644 index 000000000..5bc5ea5d3 --- /dev/null +++ b/packages/access-api/src/types/ucanto.ts @@ -0,0 +1,8 @@ +import * as Ucanto from '@ucanto/interface' + +export type ServiceInvoke< + Service extends Record, + InvocationCapabilities extends Ucanto.Capability = Ucanto.Capability +> = ( + invocation: Ucanto.ServiceInvocation +) => Promise> diff --git a/packages/access-api/test/access-authorize.test.js b/packages/access-api/test/access-authorize.test.js index 9a5230ba8..50d3820cc 100644 --- a/packages/access-api/test/access-authorize.test.js +++ b/packages/access-api/test/access-authorize.test.js @@ -17,6 +17,7 @@ import { } from './helpers/ucanto-test-utils.js' import { ed25519, Absentee } from '@ucanto/principal' import { delegate } from '@ucanto/core' +import { Space } from '@web3-storage/capabilities' /** @type {typeof assert} */ const t = assert @@ -312,8 +313,12 @@ describe('access/authorize', function () { const space = await ed25519.generate() const w3 = ctx.service - await registerSpaces([space], ctx) const account = Absentee.from({ id: 'did:mailto:dag.house:test' }) + await registerSpaces([space], { + ...ctx, + agent: ctx.issuer, + account, + }) // delegate all space capabilities to the account const delegation = await delegate({ @@ -342,6 +347,7 @@ describe('access/authorize', function () { }) .execute(ctx.conn) + warnOnErrorResult(delegateResult) assert.equal(delegateResult.error, undefined, 'delegation succeeded') // Now generate an agent and try to authorize with the account @@ -424,5 +430,16 @@ describe('access/authorize', function () { delegation.cid, 'delegation to an account is included' ) + + // use these delegations to do something on the space + const info = await Space.info + .invoke({ + issuer: agent, + audience: w3, + with: space.did(), + proofs: [authorization, attestation], + }) + .execute(ctx.conn) + assert.notDeepEqual(info.error, true, 'space/info did not error') }) }) diff --git a/packages/access-api/test/access-claim.test.js b/packages/access-api/test/access-claim.test.js index 6d5122234..97bc84362 100644 --- a/packages/access-api/test/access-claim.test.js +++ b/packages/access-api/test/access-claim.test.js @@ -16,6 +16,7 @@ for (const handlerVariant of /** @type {const} */ ([ spaceWithStorageProvider, ...createTesterFromContext(() => context(), { registerSpaces: [spaceWithStorageProvider], + account: { did: () => /** @type {const} */ ('did:mailto:foo') }, }), } })(), diff --git a/packages/access-api/test/access-client-agent.test.js b/packages/access-api/test/access-client-agent.test.js new file mode 100644 index 000000000..0b0da17ab --- /dev/null +++ b/packages/access-api/test/access-client-agent.test.js @@ -0,0 +1,149 @@ +/* eslint-disable no-console */ +import { context } from './helpers/context.js' +import { createTesterFromContext } from './helpers/ucanto-test-utils.js' +import * as principal from '@ucanto/principal' +import { Agent as AccessAgent } from '@web3-storage/access/agent' +import * as w3caps from '@web3-storage/capabilities' +import * as assert from 'assert' +import * as Ucanto from '@ucanto/interface' +import { createEmail } from './helpers/utils.js' +import { stringToDelegations } from '@web3-storage/access/encoding' +import * as delegationsResponse from '../src/utils/delegations-response.js' + +for (const accessApiVariant of /** @type {const} */ ([ + { + name: 'using access-api in miniflare', + ...(() => { + const account = { + did: () => /** @type {const} */ ('did:mailto:dag.house:foo'), + } + const spaceWithStorageProvider = principal.ed25519.generate() + /** @type {{to:string, url:string}[]} */ + const emails = [] + const email = createEmail(emails) + return { + spaceWithStorageProvider, + emails, + ...createTesterFromContext( + () => + context({ + globals: { + email, + }, + }), + { + account, + registerSpaces: [spaceWithStorageProvider], + } + ), + } + })(), + }, +])) { + describe(`access-client-agent ${accessApiVariant.name}`, () => { + it('can createSpace', async () => { + const accessAgent = await AccessAgent.create(undefined, { + connection: await accessApiVariant.connection, + }) + const space = await accessAgent.createSpace('test-add') + const delegations = accessAgent.proofs() + assert.equal(space.proof.cid, delegations[0].cid) + }) + it.skip('can authorize', async () => { + const accessAgent = await AccessAgent.create(undefined, { + connection: await accessApiVariant.connection, + }) + await accessAgent.authorize('example@dag.house') + }) + + it('can be used to do session authorization', async () => { + const { emails, connection, service } = accessApiVariant + const accessAgent = await AccessAgent.create(undefined, { + connection: await connection, + }) + /** @type {Ucanto.Principal>} */ + const account = { did: () => 'did:mailto:dag.house:example' } + await testSessionAuthorization( + await service, + accessAgent, + account, + emails + ) + }) + }) +} + +/** + * @typedef {import('./provider-add.test.js').AccessAuthorize} AccessAuthorize + * @typedef {import('@web3-storage/capabilities/src/types.js').AccessConfirm} AccessConfirm + * @typedef {import('./helpers/ucanto-test-utils.js').AccessService} AccessService + */ + +/** + * @param {principal.ed25519.Signer.Signer<`did:web:${string}`, principal.ed25519.Signer.UCAN.SigAlg>} service + * @param {AccessAgent} access + * @param {Ucanto.Principal>} account + * @param {{to:string, url:string}[]} emails + */ +async function testSessionAuthorization(service, access, account, emails) { + const authorizeResult = await access.invokeAndExecute( + w3caps.Access.authorize, + { + audience: access.connection.id, + with: access.issuer.did(), + nb: { + iss: account.did(), + att: [{ can: '*' }], + }, + } + ) + assert.notDeepStrictEqual( + authorizeResult.error, + true, + 'authorize result is not an error' + ) + + const latestEmail = emails.at(-1) + assert.ok(latestEmail, 'received a confirmation email') + const confirmationInvocations = stringToDelegations( + new URL(latestEmail.url).searchParams.get('ucan') ?? '' + ) + assert.deepEqual(confirmationInvocations.length, 1) + const serviceSaysAccountCanConfirm = + /** @type {Ucanto.Invocation} */ ( + confirmationInvocations[0] + ) + + const confirm = await w3caps.Access.confirm + .invoke({ + nb: { + ...serviceSaysAccountCanConfirm.capabilities[0].nb, + }, + issuer: service, + audience: access.connection.id, + with: access.connection.id.did(), + proofs: [serviceSaysAccountCanConfirm], + }) + .delegate() + + const [confirmationResult] = await access.connection.execute(confirm) + assert.notDeepStrictEqual( + confirmationResult.error, + true, + 'confirm result is not an error' + ) + + const claimResult = await access.invokeAndExecute(w3caps.Access.claim, { + with: access.issuer.did(), + }) + assert.notDeepEqual( + claimResult.error, + true, + 'access/claim result is not an error' + ) + assert.ok(!claimResult.error) + const claimedDelegations1 = [ + ...delegationsResponse.decode(claimResult.delegations), + ] + assert.ok(claimedDelegations1.length > 0, 'claimed some delegations') +} diff --git a/packages/access-api/test/access-delegate.test.js b/packages/access-api/test/access-delegate.test.js index 2dc1d66d9..a9bcb2048 100644 --- a/packages/access-api/test/access-delegate.test.js +++ b/packages/access-api/test/access-delegate.test.js @@ -6,13 +6,10 @@ import * as Ucanto from '@ucanto/interface' import * as ucanto from '@ucanto/core' import * as principal from '@ucanto/principal' import { createAccessDelegateHandler } from '../src/service/access-delegate.js' -import { createAccessClaimHandler } from '../src/service/access-claim.js' import { createDelegationsStorage, toDelegationsDict, } from '../src/service/delegations.js' -import { createD1Database } from '../src/utils/d1.js' -import { DbDelegationsStorage } from '../src/models/delegations.js' import * as delegationsResponse from '../src/utils/delegations-response.js' import { assertNotError, @@ -29,10 +26,12 @@ for (const handlerVariant of /** @type {const} */ ([ name: 'handled by access-api in miniflare', ...(() => { const spaceWithStorageProvider = principal.ed25519.generate() + const account = { did: () => /** @type {const} */ ('did:mailto:foo') } return { spaceWithStorageProvider, ...createTesterFromContext(() => context(), { registerSpaces: [spaceWithStorageProvider], + account, }), } })(), @@ -122,78 +121,16 @@ async function testCanAccessDelegateWithRegisteredSpace(options) { * Run the same tests against several variants of ( access/delegate | access/claim ) handlers. */ for (const variant of /** @type {const} */ ([ - { - name: 'handled by createAccessHandler using array createDelegationsStorage', - ...(() => { - const spaceWithStorageProvider = principal.ed25519.generate() - return { - spaceWithStorageProvider, - ...createTesterFromHandler( - (() => { - const delegations = createDelegationsStorage() - return () => { - return createAccessHandler( - createAccessDelegateHandler({ - delegations, - hasStorageProvider: async (uri) => { - return ( - uri === - (await spaceWithStorageProvider.then((s) => s.did())) - ) - }, - }), - createAccessClaimHandler({ delegations }) - ) - } - })() - ), - } - })(), - }, - { - name: 'handled by createAccessHandler using DbDelegationsStorage', - ...(() => { - const spaceWithStorageProvider = principal.ed25519.generate() - const d1 = context().then((ctx) => ctx.d1) - const database = d1.then((d1) => createD1Database(d1)) - const delegations = database.then((db) => new DbDelegationsStorage(db)) - return { - spaceWithStorageProvider, - ...createTesterFromHandler( - (() => { - return () => { - /** - * @type {InvocationHandler} - */ - return async (invocation) => { - const handle = createAccessHandler( - createAccessDelegateHandler({ - delegations: await delegations, - hasStorageProvider: async (uri) => { - return ( - uri === - (await spaceWithStorageProvider.then((s) => s.did())) - ) - }, - }), - createAccessClaimHandler({ delegations: await delegations }) - ) - return handle(invocation) - } - } - })() - ), - } - })(), - }, { name: 'handled by access-api in miniflare', ...(() => { const spaceWithStorageProvider = principal.ed25519.generate() + const account = { did: () => /** @type {const} */ ('did:mailto:foo') } return { spaceWithStorageProvider, ...createTesterFromContext(() => context(), { registerSpaces: [spaceWithStorageProvider], + account, }), } })(), @@ -443,7 +380,11 @@ async function testInsufficientStorageIfNoStorageProvider(options) { */ /** - * @param {InvocationHandler} invoke + * @typedef {import('@web3-storage/access/types').Service} AccessService + */ + +/** + * @param {import('../src/types/ucanto.js').ServiceInvoke} invoke * @param {Ucanto.Signer>} issuer * @param {Ucanto.Verifier} audience */ @@ -520,31 +461,3 @@ async function setupDelegateThenClaim(invoker, audience) { * @typedef {Ucanto.InferInvokedCapability} AccessClaim * @typedef {Ucanto.InferInvokedCapability} AccessDelegate */ - -/** - * @param {import('../src/service/access-delegate.js').AccessDelegateHandler} handleDelegate - * @param {InvocationHandler} handleClaim - * @returns {InvocationHandler} - */ -function createAccessHandler(handleDelegate, handleClaim) { - return async (invocation) => { - const can = invocation.capabilities[0].can - switch (can) { - case 'access/claim': { - return handleClaim( - /** @type {Ucanto.Invocation} */ (invocation) - ) - } - case 'access/delegate': { - return handleDelegate( - /** @type {Ucanto.Invocation} */ (invocation) - ) - } - default: { - // eslint-disable-next-line no-void - void (/** @type {never} */ (can)) - } - } - throw new Error(`unexpected can=${can}`) - } -} diff --git a/packages/access-api/test/helpers/types.ts b/packages/access-api/test/helpers/types.ts index 5c2ba1e02..c939aba7e 100644 --- a/packages/access-api/test/helpers/types.ts +++ b/packages/access-api/test/helpers/types.ts @@ -1,9 +1,9 @@ import type * as Ucanto from '@ucanto/interface' import type { Miniflare } from 'miniflare' -export interface HelperTestContext { +export interface HelperTestContext> { issuer: Ucanto.Signer> service: Ucanto.Signer> - conn: Ucanto.ConnectionView> + conn: Ucanto.ConnectionView mf: Miniflare } diff --git a/packages/access-api/test/helpers/ucanto-test-utils.js b/packages/access-api/test/helpers/ucanto-test-utils.js index e6357d32b..7d53c3acc 100644 --- a/packages/access-api/test/helpers/ucanto-test-utils.js +++ b/packages/access-api/test/helpers/ucanto-test-utils.js @@ -1,37 +1,51 @@ import * as Ucanto from '@ucanto/interface' -import { Voucher } from '@web3-storage/capabilities' +import { Access, Provider, Voucher } from '@web3-storage/capabilities' import * as assert from 'assert' import * as principal from '@ucanto/principal' - +import * as delegationsResponse from '../../src/utils/delegations-response.js' /** - * @typedef {import('./types').HelperTestContext} HelperTestContext + * @typedef {import('@web3-storage/access/types').Service} AccessService */ /** * Tests using context from "./helpers/context.js", which sets up a testable access-api inside miniflare. * - * @param {() => Promise} createContext + * @template {Record} Service + * @param {() => Promise>} createContext * @param {object} [options] - * @param {Iterable>} options.registerSpaces - spaces to register in access-api. Some access-api functionality on a space requires it to be registered. + * @param {Ucanto.Principal>} options.account - account to register spaces with + * @param {Iterable>>>} options.registerSpaces - spaces to register in access-api. Some access-api functionality on a space requires it to be registered. */ export function createTesterFromContext(createContext, options) { const context = createContext().then(async (ctx) => { - await registerSpaces(options?.registerSpaces ?? [], ctx) - return ctx + const registeredSpaceAgent = await principal.ed25519.generate() + if (options) { + await registerSpaces(options?.registerSpaces ?? [], { + ...ctx, + account: options.account, + agent: registeredSpaceAgent, + }) + } + return { + ...ctx, + registeredSpaceAgent, + } }) + /** @type {Promise>} */ + const connection = context.then((ctx) => ctx.conn) const issuer = context.then(({ issuer }) => issuer) const audience = context.then(({ service }) => service) + const service = context.then(({ service }) => service) const miniflare = context.then(({ mf }) => mf) /** - * @template {Ucanto.Capability} Capability - * @param {Ucanto.Invocation} invocation + * @type {import('../../src/types/ucanto').ServiceInvoke} */ const invoke = async (invocation) => { const { conn } = await context const [result] = await conn.execute(invocation) return result } - return { issuer, audience, invoke, miniflare } + return { issuer, audience, invoke, miniflare, context, connection, service } } /** @@ -43,23 +57,55 @@ export function createTesterFromContext(createContext, options) { * given an iterable of spaces, register them against an access-api * using a service-issued voucher/redeem invocation * - * @param {Iterable>} spaces + * @param {Iterable>>>} spaces * @param {object} options - * @param {Ucanto.Signer} options.service + * @param {Ucanto.Signer>} options.service + * @param {Ucanto.Signer>} options.agent + * @param {Ucanto.Principal>} options.account * @param {Ucanto.ConnectionView>} options.conn */ -export async function registerSpaces(spaces, { service, conn }) { +export async function registerSpaces( + spaces, + { service, conn, account, agent } +) { + // first register account + const request = await accountRegistrationInvocation( + service, + account.did(), + agent.did(), + service + ) + const results = await conn.execute(request) + assert.deepEqual( + results.length, + 1, + 'registration invocation should have 1 result' + ) + const [result] = results + assertNotError(result) + assert.ok( + 'delegations' in result, + 'registration result should have delegations' + ) + const accountDelegations = [ + ...delegationsResponse.decode(/** @type {any} */ (result.delegations)), + ] for (const spacePromise of spaces) { const space = await spacePromise - const redeem = await spaceRegistrationInvocation(service, space.did()) - const results = await conn.execute(redeem) - assert.deepEqual( - results.length, - 1, - 'registration invocation should have 1 result' - ) - const [result] = results - assertNotError(result) + const addProvider = await Provider.add + .invoke({ + issuer: agent, + audience: service, + with: account.did(), + nb: { + consumer: space.did(), + provider: service.did(), + }, + proofs: [...accountDelegations], + }) + .delegate() + const [addProviderResult] = await conn.execute(addProvider) + assertNotError(addProviderResult) } } @@ -72,7 +118,7 @@ export async function registerSpaces(spaces, { service, conn }) { * @param {Ucanto.DID} space * @param {Ucanto.Principal} audience - audience of the invocation. often is same as issuer */ -export async function spaceRegistrationInvocation( +export async function spaceRegistrationInvocationVoucher( issuer, space, audience = issuer @@ -92,6 +138,40 @@ export async function spaceRegistrationInvocation( return redeem } +/** + * get an access-api invocation that will register an account. + * This is useful e.g. because some functionality (e.g. access/delegate) + * will fail unless the space is registered. + * + * @param {Ucanto.Signer} service - issues voucher/redeem. e.g. could be the same signer as access-api env.PRIVATE_KEY + * @param {Ucanto.DID<'mailto'>} account + * @param {Ucanto.DID<'key'>} agent + * @param {Ucanto.Principal} audience - audience of the invocation. often is same as issuer + * @param {number} lifetimeInSeconds + */ +export async function accountRegistrationInvocation( + service, + account, + agent, + audience = service, + lifetimeInSeconds = 60 * 15 +) { + const register = await Access.confirm + .invoke({ + issuer: service, + audience, + with: service.did(), + lifetimeInSeconds, + nb: { + iss: account, + aud: agent, + att: [{ can: '*' }], + }, + }) + .delegate() + return register +} + /** * @param {{ error?: unknown }|null} result * @param {string} assertionMessage diff --git a/packages/access-api/test/helpers/utils.js b/packages/access-api/test/helpers/utils.js index e8bee3b09..494a9788e 100644 --- a/packages/access-api/test/helpers/utils.js +++ b/packages/access-api/test/helpers/utils.js @@ -110,3 +110,26 @@ export async function createSpace(issuer, service, conn, email) { export function isUploadApiStack(stack) { return stack.includes('file:///var/task/upload-api') } + +/** + * @typedef {import('../../src/utils/email').ValidationEmailSend} ValidationEmailSend + * @typedef {import('../../src/utils/email').Email} Email + */ + +/** + * create an Email that is useful for testing + * + * @param {Pick, 'push'>} storage + * @returns {Pick} + */ +export function createEmail(storage) { + const email = { + /** + * @param {ValidationEmailSend} email + */ + async sendValidation(email) { + storage.push(email) + }, + } + return email +} diff --git a/packages/access-api/test/provider-add.test.js b/packages/access-api/test/provider-add.test.js index 81c20ae21..d215e5d9c 100644 --- a/packages/access-api/test/provider-add.test.js +++ b/packages/access-api/test/provider-add.test.js @@ -14,8 +14,8 @@ import * as Ucanto from '@ucanto/interface' import { Access, Provider } from '@web3-storage/capabilities' import * as delegationsResponse from '../src/utils/delegations-response.js' import { createProvisions } from '../src/models/provisions.js' -import { Email } from '../src/utils/email.js' import { NON_STANDARD } from '@ipld/dag-ucan/signature' +import { createEmail } from './helpers/utils.js' for (const providerAddHandlerVariant of /** @type {const} */ ([ { @@ -86,6 +86,7 @@ for (const accessApiVariant of /** @type {const} */ ([ }), { registerSpaces: [spaceWithStorageProvider], + account: { did: () => /** @type {const} */ ('did:mailto:foo') }, } ), } @@ -273,21 +274,10 @@ for (const accessApiVariant of /** @type {const} */ ([ */ /** - * - * @param {Pick, 'push'>} storage - * @returns {Pick} + * @typedef {import('@web3-storage/capabilities/types').AccessClaim} AccessClaim + * @typedef {import('@web3-storage/capabilities/types').AccessAuthorize} AccessAuthorize + * @typedef {import('@web3-storage/capabilities/types').ProviderAdd} ProviderAdd */ -export function createEmail(storage) { - const email = { - /** - * @param {ValidationEmailSend} email - */ - async sendValidation(email) { - storage.push(email) - }, - } - return email -} /** * @param {object} options @@ -296,7 +286,7 @@ export function createEmail(storage) { * @param {Ucanto.Principal>} options.accountA * @param {Ucanto.Principal>} options.service - web3.storage service * @param {import('miniflare').Miniflare} options.miniflare - * @param {(invocation: Ucanto.Invocation) => Promise} options.invoke + * @param {import('../src/types/ucanto.js').ServiceInvoke} options.invoke * @param {ValidationEmailSend[]} options.emails */ async function testAuthorizeClaimProviderAdd(options) { @@ -393,6 +383,7 @@ async function testAuthorizeClaimProviderAdd(options) { assertNotError(providerAddAsAccountResult) const spaceStorageResult = await options.invoke( + // @ts-ignore - not in service type because only enabled while testing await ucanto .invoke({ issuer: space, diff --git a/packages/access-client/src/agent-data.js b/packages/access-client/src/agent-data.js index c6ce8cb72..b87a83184 100644 --- a/packages/access-client/src/agent-data.js +++ b/packages/access-client/src/agent-data.js @@ -1,7 +1,10 @@ import { Signer } from '@ucanto/principal' import { Signer as EdSigner } from '@ucanto/principal/ed25519' import { importDAG } from '@ucanto/core/delegation' +import { DID } from '@ucanto/core' import { CID } from 'multiformats' +import { Access } from '@web3-storage/capabilities' +import { isExpired } from './delegations.js' /** @typedef {import('./types').AgentDataModel} AgentDataModel */ @@ -17,6 +20,7 @@ export class AgentData { constructor(data, options = {}) { this.meta = data.meta this.principal = data.principal + this.sessionPrincipal = data.sessionPrincipal this.spaces = data.spaces this.delegations = data.delegations this.currentSpace = data.currentSpace @@ -35,6 +39,7 @@ export class AgentData { { meta: { name: 'agent', type: 'device', ...init.meta }, principal: init.principal ?? (await EdSigner.generate()), + sessionPrincipal: init.sessionPrincipal, spaces: init.spaces ?? new Map(), delegations: init.delegations ?? new Map(), currentSpace: init.currentSpace, @@ -74,6 +79,10 @@ export class AgentData { meta: raw.meta, // @ts-expect-error for some reason TS thinks this is a EdSigner principal: Signer.from(raw.principal), + // @ts-expect-error TODO figure out the types for this too + sessionPrincipal: raw.sessionPrincipal + ? DID.parse(raw.sessionPrincipal) + : undefined, currentSpace: raw.currentSpace, spaces: raw.spaces, delegations: dels, @@ -90,6 +99,7 @@ export class AgentData { const raw = { meta: this.meta, principal: this.principal.toArchive(), + sessionPrincipal: this.sessionPrincipal?.did(), currentSpace: this.currentSpace, spaces: this.spaces, delegations: new Map(), @@ -124,6 +134,14 @@ export class AgentData { await this.#save(this.export()) } + /** + * @param {import('@ucanto/interface').Principal>} principal + */ + async setSessionPrincipal(principal) { + this.sessionPrincipal = principal + await this.#save(this.export()) + } + /** * @param {import('@ucanto/interface').Delegation} delegation * @param {import('./types').DelegationMeta} [meta] @@ -143,4 +161,16 @@ export class AgentData { this.delegations.delete(cid.toString()) await this.#save(this.export()) } + + /** + * The current session proof. + */ + sessionProof() { + for (const { delegation } of this.delegations.values()) { + const cap = delegation.capabilities.find( + (c) => c.can === Access.session.can // TODO we should make sure this is the current session proof - we were checking nb.key but that doesn't seem to exist in the staging ucan/attest at the moment + ) + if (cap && !isExpired(delegation)) return delegation + } + } } diff --git a/packages/access-client/src/agent.js b/packages/access-client/src/agent.js index c7e350159..7659d3ff5 100644 --- a/packages/access-client/src/agent.js +++ b/packages/access-client/src/agent.js @@ -6,11 +6,15 @@ import * as Ucanto from '@ucanto/interface' import * as CAR from '@ucanto/transport/car' import * as CBOR from '@ucanto/transport/cbor' import * as HTTP from '@ucanto/transport/http' +import * as ucanto from '@ucanto/core' import { URI } from '@ucanto/validator' import { Peer } from './awake/peer.js' import * as Space from '@web3-storage/capabilities/space' import * as Voucher from '@web3-storage/capabilities/voucher' -import { stringToDelegation } from './encoding.js' +import * as Access from '@web3-storage/capabilities/access' +import * as Provider from '@web3-storage/capabilities/provider' + +import { stringToDelegation, bytesToDelegations } from './encoding.js' import { Websocket, AbortError } from './utils/ws.js' import { Signer } from '@ucanto/principal/ed25519' import { Verifier } from '@ucanto/principal' @@ -28,6 +32,59 @@ export { AgentData } const HOST = 'https://access.web3.storage' const PRINCIPAL = DID.parse('did:web:web3.storage') +/** + * + * @param {string} email + * @returns {Ucanto.Principal>} + */ +function emailToSessionPrincipal(email) { + const parts = email.split('@').map((s) => encodeURIComponent(s)) + return DID.parse(`did:mailto:${parts[1]}:${parts[0]}`) +} + +/** + * @param {Ucanto.Signer>} space + * @param {Ucanto.Principal>} account + * @returns + */ +async function createSpaceSaysAccountCanAdminSpace(space, account) { + return ucanto.delegate({ + issuer: space, + audience: account, + capabilities: [ + { + can: 'space/*', + with: space.did(), + }, + { + can: 'store/*', + with: space.did(), + }, + { + can: 'upload/*', + with: space.did(), + }, + ], + }) +} + +/** + * @param {Ucanto.Signer>} space + * @param {Ucanto.Principal>} device + */ +async function createSpaceSaysDeviceCanAccessDelegateWithSpace(space, device) { + return ucanto.delegate({ + issuer: space, + audience: device, + capabilities: [ + { + can: 'access/delegate', + with: space.did(), + }, + ], + }) +} + /** * @typedef {import('./types').Service} Service */ @@ -191,21 +248,38 @@ export class Agent { } /** - * Get all the proofs matching the capabilities + * Get all the proofs matching the capabilities. + * + * Proofs are delegations with an audience matching agent DID, or with an + * audience matching the session DID. * - * Proofs are delegations with an audience matching agent DID. + * Proof of session will also be included in the returned proofs if any + * proofs matching the passed capabilities require it. * * @param {import('@ucanto/interface').Capability[]} [caps] - Capabilities to filter by. Empty or undefined caps with return all the proofs. */ proofs(caps) { const arr = [] + const session = this.#data.sessionProof() + let hasSessionDelegations = false - for (const value of this.#delegations(caps)) { - if (value.delegation.audience.did() === this.issuer.did()) { - arr.push(value.delegation) + for (const { delegation } of this.#delegations(caps)) { + const aud = delegation.audience + if ( + aud.did() === this.issuer.did() || + aud.did() === session?.audience.did() + ) { + arr.push(delegation) + } + if (aud.did() === session?.audience.did()) { + hasSessionDelegations = true } } + if (session && hasSessionDelegations) { + arr.push(session) + } + return arr } @@ -233,7 +307,11 @@ export class Agent { const arr = [] for (const value of this.#delegations(caps)) { - if (value.delegation.audience.did() !== this.issuer.did()) { + const { delegation } = value + const isSession = delegation.capabilities.some( + (c) => c.can === Access.session.can + ) + if (!isSession && delegation.audience.did() !== this.issuer.did()) { arr.push(value) } } @@ -241,6 +319,42 @@ export class Agent { return arr } + /** + * Creates a space signer and a delegation to the agent + * + * @param {string} [name] + */ + async newCreateSpace(name) { + const signer = await Signer.generate() + const proof = await Space.top.delegate({ + issuer: signer, + audience: this.issuer, + with: signer.did(), + expiration: Infinity, + }) + + await this.addProvider(signer) + await this.delegateSpaceAccessToAccount(signer) + + /** @type {import('./types').SpaceMeta} */ + const meta = { isRegistered: true } + // eslint-disable-next-line eqeqeq + if (name != undefined) { + if (typeof name !== 'string') { + throw new TypeError('invalid name') + } + meta.name = name + } + + await this.#data.addSpace(signer.did(), meta, proof) + + return { + did: signer.did(), + meta, + proof, + } + } + /** * Creates a space signer and a delegation to the agent * @@ -409,6 +523,149 @@ export class Agent { } } + /** + * Request authorization of a session allowing this agent to issue UCANs + * signed by the passed email address. + * + * @param {string} email + * @param {object} [opts] + * @param {AbortSignal} [opts.signal] + */ + async authorize(email, opts) { + const sessionPrincipal = emailToSessionPrincipal(email) + + const res = await this.invokeAndExecute(Access.authorize, { + audience: this.connection.id, + with: this.issuer.did(), + nb: { + iss: sessionPrincipal.did(), + att: [{ can: 'store/*' }, { can: 'provider/add' }, { can: 'upload/*' }], + }, + }) + + if (res?.error) { + throw new Error('failed to authorize session', { cause: res }) + } + + const sessionDelegation = + /** @type {Ucanto.Delegation<[import('./types').AccessSession]>} */ + (await this.#waitForDelegation(opts)) + + const cap = sessionDelegation.capabilities.find( + // @ts-expect-error "key" does not exist in object, unless it's a session capability + (c) => c.can === Access.session.can && c.nb.key === this.issuer.did() + ) + if (!cap && isExpired(sessionDelegation)) { + throw new Error('received invalid delegation') + } + + await this.addProof(sessionDelegation) + this.#data.setSessionPrincipal(sessionPrincipal) + } + + async claimDelegations() { + const res = await this.invokeAndExecute(Access.claim, { + audience: this.connection.id, + with: this.issuer.did(), + }) + if (res.error) { + throw new Error('error claiming delegations') + } + const delegations = Object.values(res.delegations).flatMap((bytes) => + bytesToDelegations(bytes) + ) + for (const delegation of delegations) { + this.addProof(delegation) + + // if we can find a store/* capability in this delegation, look in the proofs + // for the concrete capabilities where space DIDs will be specified + // TODO: this was my first attempt at inferring spaces from claimed delegations, but I think it needs work - tv + // if (delegation.capabilities.some((cap) => cap.can === 'store/*')) { + // const spaceListingProof = delegation.proofs.find((del) => + // del.capabilities.some((cap) => cap.can === 'store/list') + // ) + // const spaceListingCap = spaceListingProof.capabilities.find( + // (cap) => cap.can === 'store/list' + // ) + // if (spaceListingCap) { + // this.#data.addSpace( + // spaceListingCap.with, + // { isRegistered: true }, + // delegation + // ) + // } + // } + } + + // TODO: should we be inferring which spaces we have access to here and updating local space state? + + return delegations + } + + /** + * + * @param {Signer.EdSigner} space - TODO is this type correct? + */ + async addProvider(space) { + const sessionPrincipal = this.#data.sessionPrincipal + + if (!sessionPrincipal) { + throw new Error('cannot add provider, please authorize first') + } + + return await this.invokeAndExecute(Provider.add, { + audience: this.connection.id, + with: sessionPrincipal.did(), + proofs: this.proofs([ + { + can: 'provider/add', + with: sessionPrincipal.did(), + }, + ]), + nb: { + // TODO probably need to make it possible to pass other providers in + provider: 'did:web:staging.web3.storage', + consumer: space.did(), + }, + }) + } + + /** + * + * @param {Signer.EdSigner} space - TODO is this type correct? + */ + async delegateSpaceAccessToAccount(space) { + const sessionPrincipal = this.#data.sessionPrincipal + + if (!sessionPrincipal) { + throw new Error( + 'cannot add delegate space access to account, please authorize first' + ) + } + + const spaceSaysAccountCanAdminSpace = + await createSpaceSaysAccountCanAdminSpace(space, sessionPrincipal) + return await this.invokeAndExecute(Access.delegate, { + audience: this.connection.id, + with: space.did(), + expiration: Infinity, + nb: { + delegations: { + [spaceSaysAccountCanAdminSpace.cid.toString()]: + spaceSaysAccountCanAdminSpace.cid, + }, + }, + proofs: [ + await createSpaceSaysDeviceCanAccessDelegateWithSpace( + space, + this.issuer + ), + // must be embedded here because it's referenced by cid in .nb.delegations + spaceSaysAccountCanAdminSpace, + ], + }) + } + /** * Invokes voucher/redeem for the free tier, wait on the websocket for the voucher/claim and invokes it * @@ -575,12 +832,12 @@ export class Agent { * await recoverInvocation.execute(agent.connection) * ``` * + * @type {import('./types').InvokeAndExecute} * @template {Ucanto.Ability} A * @template {Ucanto.URI} R - * @template {Ucanto.TheCapabilityParser>} CAP - * @template {Ucanto.Caveats} [C={}] - * @param {CAP} cap - * @param {import('./types').InvokeOptions} options + * @template {Ucanto.Caveats} C + * @param {Ucanto.TheCapabilityParser>} cap + * @param {import('./types').InvokeOptions>>} options */ async invokeAndExecute(cap, options) { const inv = await this.invoke(cap, options) @@ -588,7 +845,7 @@ export class Agent { // @ts-ignore const out = inv.execute(this.connection) - return /** @type {Promise, import('./types').Service>>} */ ( + return /** @type {Promise>>, import('./types').Service>>} */ ( out ) } @@ -642,9 +899,12 @@ export class Agent { async invoke(cap, options) { const space = options.with || this.currentSpace() if (!space) { - throw new Error('No space selected, you need pass a resource.') + throw new Error( + 'No space or resource selected, you need pass a resource.' + ) } + const extraProofs = options.proofs || [] const proofs = this.proofs([ { with: space, @@ -652,13 +912,15 @@ export class Agent { }, ]) - if (proofs.length === 0 && options.with !== this.did()) { + if ( + proofs.length === 0 && + options.with !== this.did() && + extraProofs.length === 0 + ) { throw new Error( - `no proofs available for resource ${space} and ability ${cap.can}` + `no proofs available for resource ${space} and ability ${cap.can} and no extra proofs were provided` ) } - - const extraProofs = options.proofs || [] const inv = invoke({ ...options, audience: options.audience || this.connection.id, diff --git a/packages/access-client/src/delegations.js b/packages/access-client/src/delegations.js index 5844701f3..3401aad5f 100644 --- a/packages/access-client/src/delegations.js +++ b/packages/access-client/src/delegations.js @@ -63,8 +63,9 @@ export function validate(delegation, opts) { */ export function canDelegateCapability(delegation, child) { for (const parent of delegation.capabilities) { + // TODO is this right? if ( - parent.with === child.with && + (parent.with === child.with || parent.with === 'ucan:*') && canDelegateAbility(parent.can, child.can) ) { return true diff --git a/packages/access-client/src/types.ts b/packages/access-client/src/types.ts index f02d85a4b..3f2fd70d0 100644 --- a/packages/access-client/src/types.ts +++ b/packages/access-client/src/types.ts @@ -22,7 +22,10 @@ import type { SignerArchive, SigAlg, Caveats, + TheCapabilityParser, + CapabilityMatch, } from '@ucanto/interface' +import * as Ucanto from '@ucanto/interface' import type { Abilities, @@ -43,6 +46,9 @@ import type { ProviderAdd, ProviderAddSuccess, ProviderAddFailure, + AccessConfirm, + AccessConfirmSuccess, + AccessConfirmFailure, } from '@web3-storage/capabilities/types' import type { SetRequired } from 'type-fest' import { Driver } from './drivers/types.js' @@ -111,6 +117,12 @@ export interface Service { access: { authorize: ServiceMethod claim: ServiceMethod + // eslint-disable-next-line @typescript-eslint/ban-types + confirm: ServiceMethod< + AccessConfirm, + AccessConfirmSuccess, + AccessConfirmFailure + > delegate: ServiceMethod< AccessDelegate, AccessDelegateSuccess, @@ -157,7 +169,8 @@ export type CIDString = string */ export interface AgentDataModel { meta: AgentMeta - principal: Signer + principal: Signer> + sessionPrincipal?: Principal> currentSpace?: DID spaces: Map delegations: Map @@ -171,6 +184,7 @@ export type AgentDataExport = Pick< 'meta' | 'currentSpace' | 'spaces' > & { principal: SignerArchive + sessionPrincipal?: string delegations: Map< CIDString, { @@ -331,4 +345,20 @@ export type EncodedDelegation = string & Phantom export type BytesDelegation = - Uint8Array & Phantom + Uint8Array & Phantom> + +export type InvokeAndExecute = < + A extends Ability, + R extends URI, + C extends Ucanto.Caveats +>( + cap: TheCapabilityParser>, + options: InvokeOptions>> +) => Promise< + Ucanto.InferServiceInvocationReturn< + Ucanto.InferInvokedCapability< + Ucanto.TheCapabilityParser> + >, + import('./types').Service + > +> diff --git a/packages/access-client/test/agent.test.js b/packages/access-client/test/agent.test.js index e88d7c225..b58d8cb62 100644 --- a/packages/access-client/test/agent.test.js +++ b/packages/access-client/test/agent.test.js @@ -129,7 +129,7 @@ describe('Agent', function () { name: 'Error', message: `no proofs available for resource ${URI.from( fixtures.alice.did() - )} and ability space/info`, + )} and ability space/info and no extra proofs were provided`, } ) }) diff --git a/packages/capabilities/src/provider.js b/packages/capabilities/src/provider.js index a29f9b634..567dadc46 100644 --- a/packages/capabilities/src/provider.js +++ b/packages/capabilities/src/provider.js @@ -8,16 +8,11 @@ * * @module */ -import { capability, DID, literal, struct } from '@ucanto/validator' +import { capability, DID, struct } from '@ucanto/validator' import { equalWith, fail, equal } from './utils.js' -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' }) -) +// e.g. did:web:web3.storage or did:web:staging.web3.storage +export const Provider = DID.match({ method: 'web' }) export const AccountDID = DID.match({ method: 'mailto' }) diff --git a/packages/capabilities/src/types.ts b/packages/capabilities/src/types.ts index cae9e031c..25442b907 100644 --- a/packages/capabilities/src/types.ts +++ b/packages/capabilities/src/types.ts @@ -36,6 +36,11 @@ export interface AccessClaimFailure { error: true } +export interface AccessConfirmSuccess { + delegations: Record> +} +export interface AccessConfirmFailure extends Ucanto.Failure {} + export type AccessDelegate = InferInvokedCapability export type AccessDelegateSuccess = unknown export type AccessDelegateFailure = { error: true } | InsufficientStorage diff --git a/packages/capabilities/test/capabilities/provider.test.js b/packages/capabilities/test/capabilities/provider.test.js index b6918b31a..e8af4546e 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: service.did(), + provider: 'did:web:test.web3.storage', 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: service.did(), + provider: 'did:web:test.web3.storage', consumer: space.did(), }) } @@ -50,7 +50,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: service.did(), + provider: 'did:web:test.web3.storage', consumer: space.did(), }, }) @@ -78,7 +78,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: service.did(), + provider: 'did:web:test.web3.storage', consumer: space.did(), }, proofs: [delegation], @@ -107,7 +107,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: service.did(), + provider: 'did:web:test.web3.storage', consumer: space.did(), }, proofs: [attestation], @@ -129,9 +129,9 @@ describe('provider/add', function () { issuer: bob, audience: service, with: bobAccount.did(), - // @ts-expect-error + // @ts-ignore nb: { - provider: service.did(), + provider: 'did:web:test.web3.storage', }, }) }, /Error: Invalid 'nb' - Object contains invalid field "consumer"/) @@ -145,7 +145,7 @@ describe('provider/add', function () { audience: service, with: bobAccount.did(), nb: { - provider: service.did(), + provider: 'did:web:test.web3.storage', // @ts-expect-error consumer: 'did:mailto:web3.storage:user', }, @@ -162,7 +162,7 @@ describe('provider/add', function () { with: bobAccount.did(), // @ts-expect-error - missing provider nb: { - // provider: service.did(), + // provider: 'did:web:test.web3.storage', consumer: bob.did(), }, }) @@ -193,7 +193,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: service.did(), + provider: 'did:web:test.web3.storage', consumer: space.did(), }, proofs: [ @@ -202,7 +202,7 @@ describe('provider/add', function () { audience: bob, with: account, nb: { - provider: service.did(), + provider: 'did:web:test.web3.storage', consumer: space.did(), }, proofs: await createAuthorization({ agent, service, account }), @@ -228,7 +228,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: service.did(), + provider: 'did:web:test.web3.storage', consumer: space.did(), }, proofs: [ @@ -237,7 +237,7 @@ describe('provider/add', function () { audience: bob, with: account, nb: { - provider: service.did(), + provider: 'did:web:test.web3.storage', }, proofs: await createAuthorization({ agent, service, account }), }), @@ -262,7 +262,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: service.did(), + provider: 'did:web:test.web3.storage', consumer: space.did(), }, proofs: [ @@ -296,7 +296,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: service.did(), + provider: 'did:web:test.web3.storage', consumer: bob.did(), }, proofs: [ @@ -331,7 +331,7 @@ describe('provider/add', function () { audience: service, with: account, nb: { - provider: service.did(), + provider: 'did:web:test.web3.storage', consumer: bob.did(), }, proofs: [ @@ -371,7 +371,7 @@ describe('provider/add', function () { audience: service, with: 'did:mailto:mallory.com:bob', nb: { - provider: service.did(), + provider: 'did:web:test.web3.storage', consumer: bob.did(), }, proofs: [ @@ -419,7 +419,7 @@ describe('provider/add', function () { with: account.did(), nb: { consumer: space.did(), - provider: service.did(), + provider: 'did:web:test.web3.storage', }, // NOTE: no proofs! })