Skip to content

Commit

Permalink
test provider/add as did:mailto
Browse files Browse the repository at this point in the history
  • Loading branch information
gobengo committed Feb 28, 2023
1 parent da6adb6 commit 2f64d27
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 26 deletions.
12 changes: 12 additions & 0 deletions packages/access-api/src/service/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as uploadApi from './upload-api-proxy.js'
import { accessAuthorizeProvider } from './access-authorize.js'
import { accessDelegateProvider } from './access-delegate.js'
import { accessClaimProvider } from './access-claim.js'
import { providerAddProvider } from './provider-add.js'

/**
* @param {import('../bindings').RouteContext} ctx
Expand Down Expand Up @@ -51,6 +52,17 @@ export function service(ctx) {
})(...args)
},
},

provider: {
add: (...args) => {
// disable until hardened in test/staging
if (ctx.config.ENV === 'production') {
throw new Error(`provider/add invocation handling is not enabled`)
}
return providerAddProvider(ctx)(...args)
},
},

voucher: {
claim: voucherClaimProvider(ctx),
redeem: voucherRedeemProvider(ctx),
Expand Down
13 changes: 13 additions & 0 deletions packages/access-api/src/service/provider-add.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as Ucanto from '@ucanto/interface'
import * as Server from '@ucanto/server'
import { Provider } from '@web3-storage/capabilities'

/**
* @typedef {import('@web3-storage/capabilities/types').ProviderAdd} ProviderAdd
* @typedef {import('@web3-storage/capabilities/types').ProviderAddSuccess} ProviderAddSuccess
* @typedef {import('@web3-storage/capabilities/types').ProviderAddFailure} ProviderAddFailure
*/
Expand All @@ -24,3 +27,13 @@ export function createProviderAddHandler() {
}
}
}

/**
* @param {import('../bindings').RouteContext} ctx
*/
export function providerAddProvider(ctx) {
return Server.provide(Provider.add, async ({ invocation }) => {
const handler = createProviderAddHandler()
return handler(/** @type {Ucanto.Invocation<ProviderAdd>} */ (invocation))
})
}
13 changes: 11 additions & 2 deletions packages/access-api/test/helpers/ucanto-test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@ import { Voucher } from '@web3-storage/capabilities'
import * as assert from 'assert'
import * as principal from '@ucanto/principal'

/**
* @typedef HelperTestContext
* @property {Ucanto.Signer<Ucanto.DID<'key'>>} issuer
* @property {Ucanto.Signer<Ucanto.DID>} service
* @property {Ucanto.ConnectionView<Record<string, any>>} conn
* @property {import('miniflare').Miniflare} mf
*/

/**
* Tests using context from "./helpers/context.js", which sets up a testable access-api inside miniflare.
*
* @param {() => Promise<{ issuer: Ucanto.Signer<Ucanto.DID<'key'>>, service: Ucanto.Signer<Ucanto.DID>, conn: Ucanto.ConnectionView<Record<string, any>> }>} createContext
* @param {() => Promise<HelperTestContext>} createContext
* @param {object} [options]
* @param {Iterable<Promise<Ucanto.Principal>>} options.registerSpaces - spaces to register in access-api. Some access-api functionality on a space requires it to be registered.
*/
Expand All @@ -17,6 +25,7 @@ export function createTesterFromContext(createContext, options) {
})
const issuer = context.then(({ issuer }) => issuer)
const audience = context.then(({ service }) => service)
const miniflare = context.then(({ mf }) => mf)
/**
* @template {Ucanto.Capability} Capability
* @param {Ucanto.Invocation<Capability>} invocation
Expand All @@ -26,7 +35,7 @@ export function createTesterFromContext(createContext, options) {
const [result] = await conn.execute(invocation)
return result
}
return { issuer, audience, invoke }
return { issuer, audience, invoke, miniflare }
}

/**
Expand Down
171 changes: 150 additions & 21 deletions packages/access-api/test/provider-add.test.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
import {
assertNotError,
createTesterFromContext,
createTesterFromHandler,
warnOnErrorResult,
} from './helpers/ucanto-test-utils.js'
import * as principal from '@ucanto/principal'
import * as provider from '@web3-storage/capabilities/provider'
import * as assert from 'assert'
import { createProviderAddHandler } from '../src/service/provider-add.js'
import { context } from './helpers/context.js'
import * as Ucanto from '@ucanto/interface'
import { Access, Provider } from '@web3-storage/capabilities'
import * as delegationsResponse from '../src/utils/delegations-response.js'

/**
* Run the same tests against several variants of access/delegate handlers.
*/
for (const handlerVariant of /** @type {const} */ ([
// {
// name: 'handled by access-api in miniflare',
// ...(() => {
// const spaceWithStorageProvider = principal.ed25519.generate()
// return {
// spaceWithStorageProvider,
// ...createTesterFromContext(() => context(), {
// registerSpaces: [spaceWithStorageProvider],
// }),
// }
// })(),
// },
for (const providerAddHandlerVariant of /** @type {const} */ ([
{
name: 'handled by access-delegate-handler',
name: 'handled by createProviderAddHandler',
...(() => {
const spaceWithStorageProvider = principal.ed25519.generate()
return {
Expand All @@ -34,15 +25,15 @@ for (const handlerVariant of /** @type {const} */ ([
})(),
},
])) {
describe(`provider/add ${handlerVariant.name}`, () => {
describe(`provider/add ${providerAddHandlerVariant.name}`, () => {
it(`can be invoked`, async () => {
const space = await principal.ed25519.generate()
const issuer = await handlerVariant.issuer
const result = await handlerVariant.invoke(
const issuer = await providerAddHandlerVariant.issuer
const result = await providerAddHandlerVariant.invoke(
await provider.add
.invoke({
issuer,
audience: await handlerVariant.audience,
audience: await providerAddHandlerVariant.audience,
with: `did:mailto:example.com:foo`,
nb: {
consumer: space.did(),
Expand All @@ -56,3 +47,141 @@ for (const handlerVariant of /** @type {const} */ ([
})
})
}

for (const accessApiVariant of /** @type {const} */ ([
{
name: 'handled by access-api in miniflare',
...(() => {
const spaceWithStorageProvider = principal.ed25519.generate()
return {
spaceWithStorageProvider,
...createTesterFromContext(() => context(), {
registerSpaces: [spaceWithStorageProvider],
}),
}
})(),
},
])) {
describe(`provider/add ${accessApiVariant.name}`, () => {
it(`can invoke as did:mailto after authorize confirmation`, async () => {
await testAuthorizeClaimProviderAdd({
deviceA: await principal.ed25519.generate(),
accountA: {
did: () => /** @type {const} */ (`did:mailto:example.com:foo`),
},
space: await principal.ed25519.generate(),
invoke: accessApiVariant.invoke,
service: await accessApiVariant.audience,
miniflare: await accessApiVariant.miniflare,
})
})
})
}

/**
* @param {object} options
* @param {Ucanto.Signer<Ucanto.DID<'key'>>} options.deviceA
* @param {Ucanto.Signer<Ucanto.DID<'key'>>} options.space
* @param {Ucanto.Principal<Ucanto.DID<'mailto'>>} options.accountA
* @param {Ucanto.Principal} options.service - web3.storage service
* @param {import('miniflare').Miniflare} options.miniflare
* @param {(invocation: Ucanto.Invocation<Ucanto.Capability>) => Promise<unknown>} options.invoke
*/
async function testAuthorizeClaimProviderAdd(options) {
const { accountA, deviceA, miniflare, service, space } = options
// authorize
const confirmationUrl = await options.invoke(
await Access.authorize
.invoke({
issuer: deviceA,
audience: service,
with: deviceA.did(),
nb: {
as: accountA.did(),
},
})
.delegate()
)
assert.ok(typeof confirmationUrl === 'string', 'confirmationUrl is string')
const confirmEmailPostResponse = await miniflare.dispatchFetch(
new URL(confirmationUrl),
{ method: 'POST' }
)
assert.deepEqual(
confirmEmailPostResponse.status,
200,
'confirmEmailPostResponse status is 200'
)

// claim as deviceA
const claimAsDeviceAResult = await options.invoke(
await Access.claim
.invoke({
issuer: deviceA,
audience: service,
with: deviceA.did(),
})
.delegate()
)
assert.ok(
claimAsDeviceAResult && typeof claimAsDeviceAResult === 'object',
`claimAsDeviceAResult is an object`
)
warnOnErrorResult(claimAsDeviceAResult)
assert.ok(
'delegations' in claimAsDeviceAResult &&
typeof claimAsDeviceAResult.delegations === 'object' &&
claimAsDeviceAResult.delegations,
'claimAsDeviceAResult should have delegations property'
)
const claimedDelegations = [
...delegationsResponse.decode(
/** @type {Record<string,Ucanto.ByteView<Ucanto.Delegation>>} */ (
claimAsDeviceAResult.delegations
)
),
]
assert.deepEqual(claimedDelegations.length, 1)
const [sessionEnvelope] = claimedDelegations
assert.deepEqual(sessionEnvelope.capabilities.length, 1)
assert.deepEqual(
sessionEnvelope.proofs.length,
1,
'session envelope has session delegation as proof'
)
const [session] = sessionEnvelope.proofs
assert.ok('cid' in session, 'session proof is whole delegation not link')
assert.deepEqual(session.capabilities.length, 1, 'session has a capability')
assert.deepEqual(
session.capabilities[0].can,
'./update',
'session capability is ./update'
)
assert.deepEqual(
session.capabilities[0].with,
service.did(),
'session capability with is service'
)

// provider/add
const providerAddAsAccountResult = await options.invoke(
await Provider.add
.invoke({
issuer: deviceA.withDID(accountA.did()),
audience: service,
with: accountA.did(),
nb: {
provider: 'did:web:web3.storage:providers:w3up-alpha',
consumer: space.did(),
},
proofs: [session],
})
.delegate()
)
assert.ok(
providerAddAsAccountResult &&
typeof providerAddAsAccountResult === 'object',
`providerAddAsAccountResult is an object`
)
assertNotError(providerAddAsAccountResult)
}
6 changes: 6 additions & 0 deletions packages/access-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ import type {
AccessClaim,
AccessClaimSuccess,
AccessClaimFailure,
ProviderAdd,
ProviderAddSuccess,
ProviderAddFailure,
} from '@web3-storage/capabilities/types'
import type { SetRequired } from 'type-fest'
import { Driver } from './drivers/types.js'
Expand Down Expand Up @@ -103,6 +106,9 @@ export interface Service {
AccessDelegateFailure
>
}
provider: {
add: ServiceMethod<ProviderAdd, ProviderAddSuccess, ProviderAddFailure>
}
voucher: {
claim: ServiceMethod<
VoucherClaim,
Expand Down
23 changes: 20 additions & 3 deletions packages/capabilities/test/capabilities/provider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { access } from '@ucanto/validator'
import { Verifier } from '@ucanto/principal/ed25519'
import * as Provider from '../../src/provider.js'
import { bob, service, mallory } from '../helpers/fixtures.js'
import * as ucanto from '@ucanto/core'

describe('provider/add', function () {
it('should self issue', async function () {
const account = mallory
it('can invoke as account with ./update', async function () {
const account = mallory.withDID('did:mailto:mallory.com:mallory')
const space = bob
const auth = Provider.add.invoke({
issuer: account,
Expand All @@ -16,6 +17,21 @@ describe('provider/add', function () {
provider: 'did:web:web3.storage:providers:w3up-alpha',
consumer: space.did(),
},
proofs: [
await ucanto.delegate({
issuer: service,
audience: account,
capabilities: [
{
with: service.did(),
can: './update',
nb: {
key: mallory.did(),
},
},
],
}),
],
})

const result = await access(await auth.delegate(), {
Expand All @@ -36,11 +52,12 @@ describe('provider/add', function () {
})

it('should not support undefined consumer', async function () {
const bobAccount = bob.withDID('did:mailto:bob.com:bob')
assert.throws(() => {
Provider.add.invoke({
issuer: bob,
audience: service,
with: bob.did(),
with: bobAccount.did(),
// @ts-expect-error
nb: {
provider: 'did:web:web3.storage:providers:w3up-alpha',
Expand Down

0 comments on commit 2f64d27

Please sign in to comment.