-
Notifications
You must be signed in to change notification settings - Fork 22
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!: implement new account-based multi-device flow #433
Changes from 13 commits
ced0e0d
ddd3d0c
6b1d820
daace23
ba1ce34
941bb4d
92f7caa
75e243f
4ceb10b
7df2133
86d77a1
0471e89
098d488
78cb8bf
f4ad371
95a1c4a
5c2bc71
2156f23
6e89c42
dfe264d
459aa6f
1c79d0b
e5e9a43
0cc4e47
6d942f1
1ea805e
50402a4
53c63b8
c0672ca
9aa3a26
36628f3
a637d04
08db79b
7aa68d4
48903aa
3d70dd9
14390af
ea385ba
e28f993
50960f0
a66a294
6885743
1e9e70f
0432309
97669c5
0b2e268
00d125c
5d5f577
8ebf0b9
c0e1bb5
0d8dd30
4e74674
3af8816
2658b44
9ca0df7
571713d
d1ea1b0
70b663d
943458d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import * as Ucanto from '@ucanto/interface' | ||
import * as ucanto from '@ucanto/core' | ||
import { Verifier, 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' | ||
|
||
/** | ||
* @typedef {import('@web3-storage/capabilities/types').AccessConfirmSuccess} AccessConfirmSuccess | ||
* @typedef {import('@web3-storage/capabilities/types').AccessConfirmFailure} AccessConfirmFailure | ||
*/ | ||
|
||
/** | ||
* @param {Ucanto.Invocation<import('@web3-storage/capabilities/src/types').AccessConfirm>} 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 = Verifier.parse(capability.nb.aud) | ||
return { | ||
account, | ||
agent, | ||
} | ||
} | ||
|
||
/** | ||
* @param {Ucanto.Invocation<import('@web3-storage/capabilities/src/types').AccessConfirm>} invocation | ||
* @param {import('../bindings').RouteContext} ctx | ||
* @returns {Promise<Ucanto.Result<AccessConfirmSuccess, AccessConfirmFailure>>} | ||
*/ | ||
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:*'), | ||
})) | ||
) | ||
|
||
// 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( | ||
ctx.models.delegations.find({ | ||
audience: account.did(), | ||
}) | ||
), | ||
}) | ||
|
||
const attestation = await Access.session.delegate({ | ||
issuer: ctx.signer, | ||
audience: agent, | ||
with: ctx.signer.did(), | ||
nb: { proof: delegation.cid }, | ||
expiration: 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]), | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import * as Ucanto from '@ucanto/interface' | ||
|
||
export type ServiceInvoke< | ||
Service extends Record<string, any>, | ||
InvocationCapabilities extends Ucanto.Capability = Ucanto.Capability | ||
> = <Capability extends InvocationCapabilities>( | ||
invocation: Ucanto.ServiceInvocation<Capability> | ||
) => Promise<Ucanto.InferServiceInvocationReturn<Capability, Service>> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
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 assert from 'assert' | ||
|
||
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() | ||
return { | ||
spaceWithStorageProvider, | ||
...createTesterFromContext(context, { | ||
account, | ||
registerSpaces: [spaceWithStorageProvider], | ||
}), | ||
} | ||
})(), | ||
}, | ||
])) { | ||
describe(`access-client-agent ${accessApiVariant.name}`, () => { | ||
it('can createSpace', async () => { | ||
const accessAgent = await AccessAgent.create(undefined, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Do not yet know There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree but didn't want to change API here |
||
connection: await accessApiVariant.connection, | ||
}) | ||
const space = await accessAgent.createSpace('test-add') | ||
const delegations = accessAgent.proofs() | ||
assert.equal(space.proof.cid, delegations[0].cid) | ||
}) | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe something more descriptive here like:
"can not authorize this session, probably link in the email has expired ${confirmation}"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gobengo