Skip to content

Commit

Permalink
feat: space/info will not error for spaces that have had storage prov…
Browse files Browse the repository at this point in the history
…ider added via provider/add (#510)

Motivation:
* #497 
* @Gozala and I talked about doing this minimally just to make sure that
when w3infra calls this as part of handling `store/add` that it will
work.

---------

Co-authored-by: Irakli Gozalishvili <[email protected]>
  • Loading branch information
gobengo and Gozala authored Mar 8, 2023
1 parent 5e85c0d commit ea4e872
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 20 deletions.
4 changes: 2 additions & 2 deletions packages/access-api/src/models/spaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { D1Dialect } from 'kysely-d1'
import { D1Error, GenericPlugin } from '../utils/d1.js'

/**
* @typedef {import('@web3-storage/access/src/types.js').SpaceRecord} SpaceRecord
* @typedef {import('@web3-storage/access/src/types.js').SpaceTable} SpaceTable
*/

/**
Expand All @@ -19,7 +19,7 @@ export class Spaces {
* @param {D1Database} d1
*/
constructor(d1) {
/** @type {GenericPlugin<SpaceRecord>} */
/** @type {GenericPlugin<import('kysely').Selectable<SpaceTable>>} */
const objectPlugin = new GenericPlugin({
metadata: (v) => {
// this will be `EMPTY` because it's the default value in the sql schema
Expand Down
78 changes: 62 additions & 16 deletions packages/access-api/src/service/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as ucanto from '@ucanto/core'
import * as Ucanto from '@ucanto/interface'
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 { top } from '@web3-storage/capabilities/top'
Expand All @@ -15,6 +16,7 @@ import { accessAuthorizeProvider } from './access-authorize.js'
import { accessDelegateProvider } from './access-delegate.js'
import { accessClaimProvider } from './access-claim.js'
import { providerAddProvider } from './provider-add.js'
import { Spaces } from '../models/spaces.js'

/**
* @param {import('../bindings').RouteContext} ctx
Expand All @@ -24,6 +26,9 @@ import { providerAddProvider } from './provider-add.js'
* }
*/
export function service(ctx) {
/** @param {Ucanto.DID<'key'>} space */
const hasStorageProvider = async (space) =>
spaceHasStorageProvider(space, ctx.models.spaces, ctx.models.provisions)
return {
store: uploadApi.createStoreProxy(ctx),
upload: uploadApi.createUploadProxy(ctx),
Expand All @@ -47,19 +52,7 @@ export function service(ctx) {
}
return accessDelegateProvider({
delegations: ctx.models.delegations,
hasStorageProvider: async (space) => {
/** @type {import('./access-delegate.js').HasStorageProvider} */
const registeredViaVoucherRedeem = async (space) =>
Boolean(await ctx.models.spaces.get(space))
/** @type {import('./access-delegate.js').HasStorageProvider} */
// eslint-disable-next-line unicorn/consistent-function-scoping
const registeredViaProviderAdd = (space) =>
ctx.models.provisions.hasStorageProvider(space)
return Boolean(
(await registeredViaProviderAdd(space)) ||
(await registeredViaVoucherRedeem(space))
)
},
hasStorageProvider,
})(...args)
},
},
Expand All @@ -81,8 +74,27 @@ export function service(ctx) {

space: {
info: Server.provide(Space.info, async ({ capability, invocation }) => {
const results = await ctx.models.spaces.get(capability.with)
if (!results) {
const spaceDid = capability.with
if (!validator.DID.match({ method: 'key' }).is(spaceDid)) {
/** @type {import('@web3-storage/access/types').SpaceUnknown} */
const unexpectedSpaceDidFailure = {
error: true,
name: 'SpaceUnknown',
message: `can only get info for did:key spaces`,
}
return unexpectedSpaceDidFailure
}
if (
await spaceHasStorageProviderFromProviderAdd(
spaceDid,
ctx.models.provisions
)
) {
return { did: spaceDid }
}
// this only exists if the space was registered via voucher/redeem
const space = await ctx.models.spaces.get(capability.with)
if (!space) {
/** @type {import('@web3-storage/access/types').SpaceUnknown} */
const spaceUnknownFailure = {
error: true,
Expand All @@ -91,7 +103,8 @@ export function service(ctx) {
}
return spaceUnknownFailure
}
return results
/** @type {import('@web3-storage/access/types').SpaceRecord} */
return space
}),
recover: Server.provide(
Space.recover,
Expand Down Expand Up @@ -219,3 +232,36 @@ export function service(ctx) {
},
}
}

/**
* @param {Ucanto.DID<'key'>} space
* @param {Spaces} spaces
* @param {import('../types/provisions.js').ProvisionsStorage} provisions
* @returns {Promise<boolean>}
*/
async function spaceHasStorageProvider(space, spaces, provisions) {
return (
(await spaceHasStorageProviderFromProviderAdd(space, provisions)) ||
(await spaceHasStorageProviderFromVoucherRedeem(space, spaces))
)
}

/**
* @param {Ucanto.DID<'key'>} space
* @param {Spaces} spaces
* @returns {Promise<boolean>}
*/
async function spaceHasStorageProviderFromVoucherRedeem(space, spaces) {
const registered = Boolean(await spaces.get(space))
return registered
}

/**
* @param {Ucanto.DID<'key'>} space
* @param {import('../types/provisions.js').ProvisionsStorage} provisions
* @returns {Promise<boolean>}
*/
async function spaceHasStorageProviderFromProviderAdd(space, provisions) {
const registeredViaProviderAdd = await provisions.hasStorageProvider(space)
return registeredViaProviderAdd
}
122 changes: 121 additions & 1 deletion packages/access-api/test/provider-add.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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'

for (const providerAddHandlerVariant of /** @type {const} */ ([
{
Expand Down Expand Up @@ -64,7 +65,11 @@ for (const accessApiVariant of /** @type {const} */ ([
/** @type {{to:string, url:string}[]} */
const emails = []
const email = createEmail(emails)
const features = new Set(['provider/add', 'access/delegate'])
const features = new Set([
'provider/add',
'access/delegate',
'store/info',
])
return {
spaceWithStorageProvider,
emails,
Expand Down Expand Up @@ -187,6 +192,77 @@ for (const accessApiVariant of /** @type {const} */ ([
assertNotError(accessDelegateResult)
})
}

if (
['provider/add', 'store/info'].every((f) =>
accessApiVariant.features.has(f)
)
) {
it('provider/add allows for store/info ', async () => {
const space = await principal.ed25519.generate()
const agent = await accessApiVariant.issuer
const service = await accessApiVariant.audience
const accountDid = /** @type {const} */ ('did:mailto:example.com:foo')
const accountAuthorization = await createAccountAuthorization(
agent,
service,
principal.Absentee.from({
id: /** @type {Ucanto.DID<'mailto'>} */ (accountDid),
})
)
const addStorageProvider = await ucanto
.invoke({
issuer: agent,
audience: service,
capability: {
can: 'provider/add',
with: accountDid,
nb: {
provider: 'did:web:web3.storage:providers:w3up-alpha',
consumer: space.did(),
},
},
proofs: [...accountAuthorization],
})
.delegate()
const addStorageProviderResult = await accessApiVariant.invoke(
addStorageProvider
)
assertNotError(addStorageProviderResult)

// storage provider added. So we should be able to space/info now
const spaceInfo = await ucanto
.invoke({
issuer: agent,
audience: service,
capability: {
can: 'space/info',
with: space.did(),
nb: {
delegations: {},
},
},
proofs: [
// space says agent can store/info with space
await ucanto.delegate({
issuer: space,
audience: agent,
capabilities: [
{
can: 'space/info',
with: space.did(),
},
],
}),
],
})
.delegate()
const spaceInfoResult = await accessApiVariant.invoke(spaceInfo)
assertNotError(spaceInfoResult)
assert.ok('did' in spaceInfoResult)
assert.deepEqual(spaceInfoResult.did, space.did())
})
}
}

/**
Expand Down Expand Up @@ -337,3 +413,47 @@ async function testAuthorizeClaimProviderAdd(options) {
`testing/space-storage.hasStorageProvider is true`
)
}

/**
* Create some proofs that delegate capabilities to agent to invoke on behalf of account.
* This is supposed to emulate what gets created by `access/authorize` confirmation email link click.
*
* @param {Ucanto.Principal<Ucanto.DID<'key'>>} agent - device agent that will be authorized
* @param {Ucanto.Signer<Ucanto.DID>} service
* @param {Ucanto.UCAN.Signer<Ucanto.DID<'mailto'>, NON_STANDARD>} account
* @param {Ucanto.Capabilities} capabilities
* @returns
*/
async function createAccountAuthorization(
agent,
service,
account,
capabilities = [
{
with: 'ucan:*',
can: '*',
},
]
) {
const accountAuthorizesAgentClaim = await ucanto.delegate({
issuer: account,
audience: agent,
capabilities,
})
const serviceAttestsThatAccountAuthorizesAgent = await ucanto.delegate({
issuer: service,
audience: agent,
capabilities: [
{
with: service.did(),
can: 'ucan/attest',
nb: { proof: accountAuthorizesAgentClaim.cid },
},
],
})
const proofs = [
accountAuthorizesAgentClaim,
serviceAttestsThatAccountAuthorizesAgent,
]
return proofs
}
12 changes: 11 additions & 1 deletion packages/access-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ export interface SpaceTable {
}
export type SpaceRecord = Selectable<SpaceTable>

export type SpaceInfoResult =
// w3up spaces registered via provider/add will have this
| {
// space did
did: DID<'key'>
}
// deprecated and may be removed if voucher/redeem is removed
/** @deprecated */
| SpaceRecord

export interface AccountTable {
did: URI<'did:'>
inserted_at: Generated<Date>
Expand Down Expand Up @@ -119,7 +129,7 @@ export interface Service {
redeem: ServiceMethod<VoucherRedeem, void, Failure>
}
space: {
info: ServiceMethod<SpaceInfo, SpaceRecord, Failure | SpaceUnknown>
info: ServiceMethod<SpaceInfo, SpaceInfoResult, Failure | SpaceUnknown>
'recover-validation': ServiceMethod<
SpaceRecoverValidation,
EncodedDelegation<[SpaceRecover]> | undefined,
Expand Down

0 comments on commit ea4e872

Please sign in to comment.