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: use new ucanto allows api for canDelegateCapability and implement space registration #539

Merged
2 changes: 1 addition & 1 deletion packages/access-client/src/agent-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class AgentData {
}

/**
* @param {import('@ucanto/interface').DID} did
* @param {import('@ucanto/interface').DID<'key'>} did
*/
async setCurrentSpace(did) {
this.currentSpace = did
Expand Down
156 changes: 48 additions & 108 deletions packages/access-client/src/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ 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 * as Access from '@web3-storage/capabilities/access'
import * as Provider from '@web3-storage/capabilities/provider'

Expand Down Expand Up @@ -43,43 +42,49 @@ function emailToSessionPrincipal(email) {
}

/**
* @param {Ucanto.Signer<Ucanto.DID<'key'>>} space
* @param {Ucanto.Signer<Ucanto.DID<'key'>>} issuer
* @param {Ucanto.DID} space
* @param {Ucanto.Principal<Ucanto.DID<'mailto'>>} account
* @returns
*/
async function createSpaceSaysAccountCanAdminSpace(space, account) {
async function createIssuerSaysAccountCanAdminSpace(issuer, space, account) {
return ucanto.delegate({
issuer: space,
issuer,
audience: account,
capabilities: [
{
can: 'space/*',
with: space.did(),
with: space,
},
{
can: 'store/*',
with: space.did(),
with: space,
},
{
can: 'upload/*',
with: space.did(),
with: space,
},
],
})
}

/**
* @param {Ucanto.Signer<Ucanto.DID<'key'>>} space
* @param {Ucanto.Signer<Ucanto.DID<'key'>>} issuer
* @param {Ucanto.DID} space
* @param {Ucanto.Principal<Ucanto.DID<'key'>>} device
*/
async function createSpaceSaysDeviceCanAccessDelegateWithSpace(space, device) {
async function createIssuerSaysDeviceCanAccessDelegateWithSpace(
issuer,
space,
device
) {
return ucanto.delegate({
issuer: space,
issuer,
audience: device,
capabilities: [
{
can: 'access/delegate',
with: space.did(),
with: space,
},
],
})
Expand Down Expand Up @@ -319,42 +324,6 @@ 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
*
Expand Down Expand Up @@ -466,7 +435,7 @@ export class Agent {
*
* Other methods will default to use the current space if no resource is defined
*
* @param {Ucanto.DID} space
* @param {Ucanto.DID<'key'>} space
*/
async setCurrentSpace(space) {
const proofs = this.proofs([
Expand Down Expand Up @@ -561,6 +530,10 @@ export class Agent {

await this.addProof(sessionDelegation)
this.#data.setSessionPrincipal(sessionPrincipal)

// claim delegations here because we will need an ucan/attest from the service to
// pair with the session delegation we just claimed to make it work
await this.claimDelegations()
}

async claimDelegations() {
Expand Down Expand Up @@ -603,8 +576,7 @@ export class Agent {
}

/**
*
* @param {Signer.EdSigner} space - TODO is this type correct?
* @param {Ucanto.DID<'key'>} space
*/
async addProvider(space) {
const sessionPrincipal = this.#data.sessionPrincipal
Expand All @@ -613,7 +585,7 @@ export class Agent {
throw new Error('cannot add provider, please authorize first')
}

return await this.invokeAndExecute(Provider.add, {
return this.invokeAndExecute(Provider.add, {
audience: this.connection.id,
with: sessionPrincipal.did(),
proofs: this.proofs([
Expand All @@ -625,14 +597,14 @@ export class Agent {
nb: {
// TODO probably need to make it possible to pass other providers in
provider: 'did:web:staging.web3.storage',
consumer: space.did(),
consumer: space,
},
})
}

/**
*
* @param {Signer.EdSigner} space - TODO is this type correct?
* @param {Ucanto.DID<'key'>} space
*/
async delegateSpaceAccessToAccount(space) {
const sessionPrincipal = this.#data.sessionPrincipal
Expand All @@ -644,10 +616,14 @@ export class Agent {
}

const spaceSaysAccountCanAdminSpace =
await createSpaceSaysAccountCanAdminSpace(space, sessionPrincipal)
return await this.invokeAndExecute(Access.delegate, {
await createIssuerSaysAccountCanAdminSpace(
this.issuer,
space,
sessionPrincipal
)
return this.invokeAndExecute(Access.delegate, {
audience: this.connection.id,
with: space.did(),
with: space,
expiration: Infinity,
nb: {
delegations: {
Expand All @@ -656,7 +632,8 @@ export class Agent {
},
},
proofs: [
await createSpaceSaysDeviceCanAccessDelegateWithSpace(
await createIssuerSaysDeviceCanAccessDelegateWithSpace(
this.issuer,
space,
this.issuer
),
Expand All @@ -671,13 +648,11 @@ export class Agent {
*
* It also adds a full space delegation to the service in the voucher/claim invocation to allow for recovery
*
* @param {string} email
* @param {object} [opts]
* @param {AbortSignal} [opts.signal]
*/
async registerSpace(email, opts) {
async registerSpace(opts) {
const space = this.currentSpace()
const service = this.connection.id
const spaceMeta = space ? this.#data.spaces.get(space) : undefined

if (!space || !spaceMeta) {
Expand All @@ -687,58 +662,23 @@ export class Agent {
if (spaceMeta && spaceMeta.isRegistered) {
throw new Error('Space already registered with web3.storage.')
}

const inv = await this.invokeAndExecute(Voucher.claim, {
nb: {
identity: URI.from(`mailto:${email}`),
product: 'product:free',
service: service.did(),
},
})

if (inv && inv.error) {
throw new Error('Voucher claim failed', { cause: inv })
const providerResult = await this.addProvider(
/** @type {Ucanto.DID<'key'>} */ (space)
)
if (providerResult.error) {
throw new Error(providerResult.message, { cause: providerResult })
}

const voucherRedeem =
/** @type {Ucanto.Delegation<[import('./types').VoucherRedeem]>} */ (
await this.#waitForDelegation(opts)
)
await this.addProof(voucherRedeem)
const delegationToService = await this.delegate({
abilities: ['*'],
audience: service,
expiration: Infinity,
audienceMeta: {
name: 'w3access',
type: 'service',
},
})

const accInv = await this.invokeAndExecute(Voucher.redeem, {
with: URI.from(service.did()),
nb: {
space,
identity: voucherRedeem.capabilities[0].nb.identity,
product: voucherRedeem.capabilities[0].nb.product,
},
proofs: [delegationToService],
facts: [
{
space: spaceMeta,
agent: this.meta,
},
],
})

if (accInv && accInv.error) {
throw new Error('Space registration failed', { cause: accInv })
const delegateSpaceAccessResult = await this.delegateSpaceAccessToAccount(
space
)
if (delegateSpaceAccessResult.error) {
// @ts-ignore it's very weird that this is throwing an error but line 692 above does not - ignore for now
throw new Error(delegateSpaceAccessResult.message, {
cause: delegateSpaceAccessResult,
})
}

spaceMeta.isRegistered = true

this.#data.addSpace(space, spaceMeta)
this.#data.removeDelegation(voucherRedeem.cid)
}

/**
Expand Down
21 changes: 13 additions & 8 deletions packages/access-client/src/delegations.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @ts-ignore
// eslint-disable-next-line no-unused-vars
import * as Ucanto from '@ucanto/interface'
import * as ucanto from '@ucanto/core'
import { canDelegateAbility } from '@web3-storage/capabilities/utils'

/**
Expand Down Expand Up @@ -44,7 +45,9 @@ export function validate(delegation, opts) {
} = opts ?? {}

if (checkAudience && delegation.audience.did() !== checkAudience.did()) {
throw new Error(`Delegation audience does not match required DID.`)
throw new Error(
`Delegation audience ${delegation.audience.did()} does not match required DID ${checkAudience.did()}`
)
}

if (checkIsExpired && isExpired(delegation)) {
Expand All @@ -62,13 +65,15 @@ export function validate(delegation, opts) {
* @param {import('@ucanto/interface').Capability} child
*/
export function canDelegateCapability(delegation, child) {
for (const parent of delegation.capabilities) {
// TODO is this right?
if (
(parent.with === child.with || parent.with === 'ucan:*') &&
canDelegateAbility(parent.can, child.can)
) {
return true
const allowsCapabilities = ucanto.Delegation.allows(delegation)
if (allowsCapabilities[child.with]) {
const cans = /** @type {import('@ucanto/interface').Ability[]} */ (
Object.keys(allowsCapabilities[child.with])
)
for (const can of cans) {
if (canDelegateAbility(can, child.can)) {
return true
}
}
}
return false
Expand Down
2 changes: 1 addition & 1 deletion packages/access-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export interface AgentDataModel {
meta: AgentMeta
principal: Signer<DID<'key'>>
sessionPrincipal?: Principal<Ucanto.DID<'mailto'>>
currentSpace?: DID
currentSpace?: DID<'key'>
spaces: Map<DID, SpaceMeta>
delegations: Map<CIDString, { meta: DelegationMeta; delegation: Delegation }>
}
Expand Down
2 changes: 1 addition & 1 deletion packages/access-client/test/agent-data.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ describe('AgentData', () => {
}
const store = new Store()
const data = await AgentData.create(undefined, { store })
await assert.doesNotReject(data.setCurrentSpace('did:x:y'))
await assert.doesNotReject(data.setCurrentSpace('did:key:y'))
})
})