diff --git a/packages/access-api/src/routes/raw.js b/packages/access-api/src/routes/raw.js index 648dc903a..1bab0f125 100644 --- a/packages/access-api/src/routes/raw.js +++ b/packages/access-api/src/routes/raw.js @@ -19,7 +19,7 @@ export async function postRaw(request, env) { const rsp = await server.request({ body: new Uint8Array(await request.arrayBuffer()), - headers: Object.fromEntries(request.headers.entries()), + headers: request.headers, }) return new Response(rsp.body, { headers: rsp.headers }) } diff --git a/packages/access-api/src/routes/root.js b/packages/access-api/src/routes/root.js index 5b96d0e07..71fffa539 100644 --- a/packages/access-api/src/routes/root.js +++ b/packages/access-api/src/routes/root.js @@ -20,7 +20,7 @@ export async function postRoot(request, env) { const rsp = await server.request({ body: new Uint8Array(await request.arrayBuffer()), - headers: Object.fromEntries(request.headers.entries()), + headers: request.headers, }) return new Response(rsp.body, { headers: rsp.headers }) } diff --git a/packages/access-api/src/service/index.js b/packages/access-api/src/service/index.js index 1a4c5654e..3a168dfd3 100644 --- a/packages/access-api/src/service/index.js +++ b/packages/access-api/src/service/index.js @@ -1,9 +1,11 @@ import * as Server from '@ucanto/server' import * as Identity from '@web3-storage/access/capabilities/identity' +import * as Account from '@web3-storage/access/capabilities/account' import { identityRegisterProvider } from './identity-register.js' import { identityValidateProvider } from './identity-validate.js' import { voucherClaimProvider } from './voucher-claim.js' import { voucherRedeemProvider } from './voucher-redeem.js' +import { Failure } from '@ucanto/server' /** * @param {import('../bindings').RouteContext} ctx @@ -23,6 +25,35 @@ export function service(ctx) { claim: voucherClaimProvider(ctx), redeem: voucherRedeemProvider(ctx), }, + + account: { + // @ts-expect-error - types from query dont match handler output + info: Server.provide(Account.info, async ({ capability }) => { + const { results } = await ctx.db.fetchOne({ + tableName: 'accounts', + fields: '*', + where: { + conditions: 'did =?1', + params: [capability.with], + }, + }) + + if (!results) { + throw new Failure('Account not found...') + } + return { + did: results.did, + agent: results.agent, + email: results.email, + product: results.product, + updated_at: results.update_at, + inserted_at: results.inserted_at, + } + }), + all: Server.provide(Account.all, async ({ capability }) => { + return capability + }), + }, // @ts-ignore testing: { pass() { diff --git a/packages/access-api/src/ucanto/client-codec.js b/packages/access-api/src/ucanto/client-codec.js index c518cedbe..2bcccad6f 100644 --- a/packages/access-api/src/ucanto/client-codec.js +++ b/packages/access-api/src/ucanto/client-codec.js @@ -5,16 +5,15 @@ import { UTF8 } from '@ucanto/transport' /** @type {import('./types.js').ClientCodec} */ export const clientCodec = { async encode(invocations, options) { - /** @type {Record} */ - const headers = {} + const headers = new Headers() const chain = await Delegation.delegate(invocations[0]) // TODO iterate over proofs and send them too // for (const ucan of chain.iterate()) { // // // } - headers.authorization = `bearer ${UCAN.format(chain.data)}` + headers.set('authorization', `bearer ${UCAN.format(chain.data)}`) return { headers, body: new Uint8Array() } }, diff --git a/packages/access-api/src/ucanto/server-codec.js b/packages/access-api/src/ucanto/server-codec.js index ecc357a0f..c0b92bb70 100644 --- a/packages/access-api/src/ucanto/server-codec.js +++ b/packages/access-api/src/ucanto/server-codec.js @@ -25,7 +25,7 @@ function multiValueHeader(headers, name) { } /** - * @param {Record} headers + * @param {Record | Headers} headers */ async function parseHeaders(headers) { const h = new Headers(headers) @@ -130,7 +130,7 @@ export const serverCodec = { */ encode(result) { return { - headers: HEADERS, + headers: new Headers(HEADERS), body: UTF8.encode(JSON.stringify(result)), } }, diff --git a/packages/access-api/test/identity-register.test.js b/packages/access-api/test/identity-register.test.js index e0cc4cb3b..3f5a004fc 100644 --- a/packages/access-api/test/identity-register.test.js +++ b/packages/access-api/test/identity-register.test.js @@ -4,7 +4,7 @@ import * as Identity from '@web3-storage/access/capabilities/identity' import { Accounts } from '../src/kvs/accounts.js' import { context, test } from './helpers/context.js' // eslint-disable-next-line no-unused-vars -import * as Types from '@ucanto/interface' +import * as Ucanto from '@ucanto/interface' test.beforeEach(async (t) => { t.context = await context() @@ -26,22 +26,20 @@ test('register', async (t) => { if (out?.error || !out) { return t.fail() } - // @ts-ignore - const ucan = UCAN.parse( - // @ts-ignore - out.delegation.replace('http://localhost:8787/validate?ucan=', '') - ) + const jwt = + /** @type UCAN.JWT<[import('@web3-storage/access/types').IdentityRegister]>} */ ( + out.delegation.replace('http://localhost:8787/validate?ucan=', '') + ) + const ucan = UCAN.parse(jwt) const root = await UCAN.write(ucan) const proof = Delegation.create({ root }) const register = Identity.register.invoke({ audience: service, issuer, - // @ts-ignore with: proof.capabilities[0].with, nb: { - // @ts-ignore - as: proof.capabilities[0].as, + as: proof.capabilities[0].nb.as, }, proofs: [proof], }) @@ -73,8 +71,7 @@ test('identify', async (t) => { if (out?.error || !out) { return } - /** @type {Types.UCAN.JWT<[import('@web3-storage/access/types').IdentityRegister]>} */ - // @ts-ignore + /** @type {Ucanto.UCAN.JWT<[import('@web3-storage/access/types').IdentityRegister]>} */ const jwt = out.delegation.replace('http://localhost:8787/validate?ucan=', '') const ucan = UCAN.parse(jwt) const root = await UCAN.write(ucan) diff --git a/packages/access-api/test/identity-validate.test.js b/packages/access-api/test/identity-validate.test.js index 9bac151d9..aa9b7aee8 100644 --- a/packages/access-api/test/identity-validate.test.js +++ b/packages/access-api/test/identity-validate.test.js @@ -68,56 +68,21 @@ test('should route correctly to identity/validate', async (t) => { if (out?.error || !out) { return t.fail() } - const ucan = UCAN.parse( - // @ts-ignore - out.delegation.replace('http://localhost:8787/validate?ucan=', '') - ) + + const jwt = + /** @type UCAN.JWT<[import('@web3-storage/access/types').IdentityRegister]>} */ ( + out.delegation.replace('http://localhost:8787/validate?ucan=', '') + ) + const ucan = UCAN.parse(jwt) t.is(ucan.audience.did(), issuer.did()) t.is(ucan.issuer.did(), service.did()) t.deepEqual(ucan.capabilities, [ { can: 'identity/register', with: 'mailto:hugo@dag.house', - as: issuer.did(), + nb: { + as: issuer.did(), + }, }, ]) }) - -// test('should route correctly to identity/validate and fail with proof', async (t) => { -// const { mf } = t.context -// const kp = await ucans.EdKeypair.create() -// const rootUcan = await ucans.build({ -// audience: kp.did(), -// issuer: serviceKp, -// capabilities: [ -// { -// can: { namespace: 'identity', segments: ['validate'] }, -// with: { scheme: 'mailto', hierPart: '*' }, -// }, -// ], -// lifetimeInSeconds: 100, -// }) -// const ucan = await ucans.build({ -// audience: serviceKp.did(), -// issuer: kp, -// capabilities: [ -// { -// can: { namespace: 'identity', segments: ['validate'] }, -// with: { scheme: 'mailto', hierPart: 'alice@mail.com' }, -// }, -// ], -// lifetimeInSeconds: 100, -// proofs: [ucans.encode(rootUcan)], -// }) -// const res = await mf.dispatchFetch('http://localhost:8787', { -// method: 'POST', -// headers: { -// Authorization: `Bearer ${ucans.encode(ucan)}`, -// }, -// }) -// const rsp = await res.json() -// t.deepEqual(rsp, { -// ok: false, -// error: { code: 'Error', message: 'Invalid capability' }, -// }) -// }) diff --git a/packages/access-api/test/voucher-claim.test.js b/packages/access-api/test/voucher-claim.test.js index 51d827f06..d9b03d7e9 100644 --- a/packages/access-api/test/voucher-claim.test.js +++ b/packages/access-api/test/voucher-claim.test.js @@ -42,11 +42,10 @@ test('should voucher/claim', async (t) => { t.deepEqual(delegation.proofs[0].issuer.did(), service.did()) t.deepEqual(delegation.proofs[0].capabilities, [ { - // TODO proof should have account with: service.did(), can: 'voucher/redeem', nb: { - account: service.did(), + account: 'did:*', identity: 'mailto:*', product: 'product:*', }, diff --git a/packages/access/src/agent.js b/packages/access/src/agent.js index 08ae946d4..b6715be52 100644 --- a/packages/access/src/agent.js +++ b/packages/access/src/agent.js @@ -9,6 +9,7 @@ import * as CBOR from '@ucanto/transport/cbor' import * as HTTP from '@ucanto/transport/http' import { delegate } from '@ucanto/core' import * as Voucher from './capabilities/voucher.js' +import * as Account from './capabilities/account.js' import { Websocket } from './utils/ws.js' import { stringToDelegation } from './encoding.js' import { URI } from '@ucanto/validator' @@ -45,9 +46,7 @@ const HOST = 'https://access-api.web3.storage' */ export async function buildConnection(principal, _fetch, url) { const rsp = await _fetch(url + 'version') - // @ts-ignore const { did } = await rsp.json() - // TODO how to parse any DID ???? const service = DID.parse(did) const connection = Client.connect({ @@ -57,7 +56,6 @@ export async function buildConnection(principal, _fetch, url) { channel: HTTP.open({ url, method: 'POST', - // @ts-ignore fetch: _fetch, }), }) @@ -80,6 +78,7 @@ export class Agent { this.fetch = opts.fetch this.connection = opts.connection this.data = opts.data + this.issuer = opts.data.principal // validate fetch implementation if (!this.fetch) { @@ -114,7 +113,7 @@ export class Agent { const data = await opts.store.load() const { connection, service } = await buildConnection( - data.agent, + data.principal, _fetch, url ) @@ -129,7 +128,7 @@ export class Agent { } did() { - return this.data.agent.did() + return this.data.principal.did() } /** @@ -140,19 +139,23 @@ export class Agent { const accDelegation = await delegate({ // @ts-ignore issuer: account, - audience: this.data.agent, + audience: this.data.principal, capabilities: [ { can: 'voucher/*', with: account.did(), }, + { + can: 'account/*', + with: account.did(), + }, ], lifetimeInSeconds: 8_600_000, }) const inv = await Voucher.claim .invoke({ - issuer: this.data.agent, + issuer: this.data.principal, audience: this.service, with: account.did(), nb: { @@ -172,7 +175,7 @@ export class Agent { const accInv = await Voucher.redeem .invoke({ - issuer: this.data.agent, + issuer: this.data.principal, audience: this.service, with: this.service.did(), nb: { @@ -251,4 +254,37 @@ export class Agent { peer(channel) { return new Peer({ agent: this, channel }) } + + /** + * @param {Ucanto.URI<"did:">} account + */ + async getAccountInfo(account) { + const proofs = isEmpty(this.data.delegations.getByResource(account)) + if (!proofs) { + throw new TypeError('No proofs for "account/info".') + } + + const inv = await Account.info + .invoke({ + issuer: this.issuer, + audience: this.service, + with: account, + proofs, + }) + .execute(this.connection) + + return inv + } +} + +/** + * @template T + * @param { Array | undefined} arr + */ +function isEmpty(arr) { + if (!Array.isArray(arr) || arr.length === 0) { + return + } + + return /** @type {T[]} */ (arr) } diff --git a/packages/access/src/awake/peer.js b/packages/access/src/awake/peer.js index dde20a09f..020576016 100644 --- a/packages/access/src/awake/peer.js +++ b/packages/access/src/awake/peer.js @@ -60,7 +60,7 @@ export class Peer { // step3 - awake/res send const ucan = await UCAN.issue({ - issuer: this.agent.data.agent, + issuer: this.agent.data.principal, audience: this.nextdid, capabilities: [{ with: 'awake:', can: '*' }], facts: [ @@ -188,7 +188,9 @@ export class Peer { // Pin signature const bytes = u8.fromString(this.nextdid.did() + this.pin.toString()) - const signed = await this.agent.data.agent.sign(await sha256.encode(bytes)) + const signed = await this.agent.data.principal.sign( + await sha256.encode(bytes) + ) this.channel.sendMsg(this.nextdid, { did: this.did, sig: u8.toString(signed, 'base64'), diff --git a/packages/access/src/capabilities/account.js b/packages/access/src/capabilities/account.js index 0ffdecce3..a76874da1 100644 --- a/packages/access/src/capabilities/account.js +++ b/packages/access/src/capabilities/account.js @@ -2,12 +2,18 @@ import { capability, URI } from '@ucanto/server' import { store } from './store.js' import { equalWith } from './utils.js' +export const all = capability({ + can: 'account/*', + with: URI.match({ protocol: 'did:' }), + derives: equalWith, +}) + /** * `account/info` can be derived from any of the `store/*` * capability that has matichng `with`. This allows store service * to identify account based on any user request. */ -export const info = store.derive({ +export const info = all.or(store).derive({ to: capability({ can: 'account/info', with: URI.match({ protocol: 'did:' }), diff --git a/packages/access/src/capabilities/types.ts b/packages/access/src/capabilities/types.ts index b383dfaa5..0d13f2f57 100644 --- a/packages/access/src/capabilities/types.ts +++ b/packages/access/src/capabilities/types.ts @@ -1,97 +1,20 @@ -import type { IPLDLink, Capability } from '@ipld/dag-ucan' import { InferInvokedCapability } from '@ucanto/interface' +import { all, info } from './account.js' import { identify, register, validate } from './identity.js' +import { add, list, remove } from './store.js' import { claim, redeem } from './voucher.js' +// Account +export type AccountInfo = InferInvokedCapability +export type AccountAll = InferInvokedCapability // Voucher Protocol -export interface VoucherClaim1 - extends Capability< - 'voucher/claim', - `did:${string}`, - { - /** - * Product ID/CID - */ - product: string - - /** - * URI for an identity to be validated - */ - identity: string - - /** - * DID of the service they wish to redeem voucher with - */ - service: `did:${string}` - } - > {} - -/** - * Can be invoked to redeem voucher. These are always issued by the service - */ -interface VoucherRedeemNB { - product: string - identity: string - account: `did:${string}` -} -export interface VoucherRedeem1 - extends Capability<'voucher/redeem', `did:${string}`, VoucherRedeemNB> { - // nb: VoucherRedeemNB -} - -// export type Capability< -// Can extends Ability = Ability, -// With extends Resource = Resource, -// Caveats extends unknown = unknown -// > = { -// with: With -// can: Can -// // nb?: Caveats -// } & (keyof Caveats extends never -// ? { nb?: { [key: string]: never } } -// : { nb: Caveats }) - -// type InferCapability> = -// T extends TheCapabilityParser -// ? Required< -// Capability -// > -// : never - export type VoucherRedeem = InferInvokedCapability export type VoucherClaim = InferInvokedCapability +// Identity export type IdentityValidate = InferInvokedCapability export type IdentityRegister = InferInvokedCapability export type IdentityIdentify = InferInvokedCapability - -// Identity -export interface IdentityValidate1 - extends Capability<'identity/validate', `did:${string}`, { as: string }> { - nb: { as: string } -} - -export interface IdentityRegister1 - extends Capability< - 'identity/register', - `${string}:${string}`, - { as: `did:${string}` } - > { - nb: { as: `did:${string}` } -} - -export interface IdentityIdentify1 - extends Capability<'identity/identify', `did:${string}`, {}> {} - // Store -export interface StoreAdd - extends Capability<'store/add', `did:${string}`, { link?: IPLDLink }> { - nb: { link?: IPLDLink } -} - -export interface StoreRemove - extends Capability<'store/remove', `did:${string}`, { link?: IPLDLink }> { - nb: { link?: IPLDLink } -} - -export interface StoreList - extends Capability<'store/list', `did:${string}`, {}> {} +export type StoreAdd = InferInvokedCapability +export type StoreRemove = InferInvokedCapability +export type StoreList = InferInvokedCapability diff --git a/packages/access/src/capabilities/voucher.js b/packages/access/src/capabilities/voucher.js index 31cda5c32..d750d20ab 100644 --- a/packages/access/src/capabilities/voucher.js +++ b/packages/access/src/capabilities/voucher.js @@ -1,11 +1,11 @@ import { capability, URI } from '@ucanto/validator' // @ts-ignore // eslint-disable-next-line no-unused-vars -import * as Types from '@ucanto/interface' +import * as Ucanto from '@ucanto/interface' import { canDelegateURI, equalWith } from './utils.js' /** - * @param {Types.Failure | true} value + * @param {Ucanto.Failure | true} value */ function fail(value) { return value === true ? undefined : value @@ -39,12 +39,24 @@ export const claim = voucher.derive({ derives: equalWith, }) -export const redeem = capability({ - can: 'voucher/redeem', - with: URI.match({ protocol: 'did:' }), - nb: { - product: URI.match({ protocol: 'product:' }), - identity: URI.match({ protocol: 'mailto:' }), - account: URI.match({ protocol: 'did:' }), - }, +export const redeem = voucher.derive({ + to: capability({ + can: 'voucher/redeem', + with: URI.match({ protocol: 'did:' }), + nb: { + product: URI.match({ protocol: 'product:' }), + identity: URI.match({ protocol: 'mailto:' }), + account: URI.match({ protocol: 'did:' }), + }, + derives: (child, parent) => { + return ( + fail(equalWith(child, parent)) || + fail(canDelegateURI(child.nb.identity, parent.nb.identity)) || + fail(canDelegateURI(child.nb.product, parent.nb.product)) || + fail(canDelegateURI(child.nb.account, parent.nb.account)) || + true + ) + }, + }), + derives: equalWith, }) diff --git a/packages/access/src/cli/cmd-create-account.js b/packages/access/src/cli/cmd-create-account.js new file mode 100644 index 000000000..0fa30d63a --- /dev/null +++ b/packages/access/src/cli/cmd-create-account.js @@ -0,0 +1,44 @@ +/* eslint-disable unicorn/no-process-exit */ +/* eslint-disable no-console */ +import inquirer from 'inquirer' +import ora from 'ora' +import { Agent } from '../agent.js' +import { StoreConf } from '../stores/store-conf.js' +import { getService } from './utils.js' + +/** + * @param {{ profile: any; env: string }} opts + */ +export async function cmdCreateAccount(opts) { + const { url } = await getService(opts.env) + const store = new StoreConf({ profile: opts.profile }) + + if (await store.exists()) { + const spinner = ora('Registering with the service').start() + const agent = await Agent.create({ + store, + url, + }) + + spinner.stopAndPersist() + const { email } = await inquirer.prompt({ + type: 'input', + name: 'email', + default: 'hugomrdias@gmail.com', + message: 'Input your email to validate:', + }) + spinner.start('Waiting for email validation...') + try { + await agent.createAccount(email) + spinner.succeed('Account has been created and register with the service.') + } catch (error) { + console.error(error) + // @ts-ignore + spinner.fail(error.message) + process.exit(1) + } + } else { + console.error('run setup command first.') + process.exit(1) + } +} diff --git a/packages/access/src/cli/cmd-link.js b/packages/access/src/cli/cmd-link.js index f16d865ce..f329ac1a7 100644 --- a/packages/access/src/cli/cmd-link.js +++ b/packages/access/src/cli/cmd-link.js @@ -14,7 +14,7 @@ import { getService } from './utils.js' * @param {string} channel * @param {{ profile: string; env: string }} opts */ -export async function linkCmd(channel, opts) { +export async function cmdLink(channel, opts) { const { url } = await getService(opts.env) const store = new StoreConf({ profile: opts.profile }) diff --git a/packages/access/src/cli/cmd-setup.js b/packages/access/src/cli/cmd-setup.js new file mode 100644 index 000000000..846ce6fbc --- /dev/null +++ b/packages/access/src/cli/cmd-setup.js @@ -0,0 +1,39 @@ +/* eslint-disable no-console */ +import inquirer from 'inquirer' +import { StoreConf } from '../stores/store-conf.js' + +/** + * @param {{ profile: any; }} opts + */ +export async function cmdSetup(opts) { + const store = new StoreConf({ profile: opts.profile }) + console.log('Path:', store.path) + + if (await store.exists()) { + console.log('Agent is already setup.') + } else { + const { name, type } = await inquirer.prompt([ + { + type: 'input', + name: 'name', + default: 'cli', + message: 'Input the name for this device:', + }, + { + type: 'list', + name: 'type', + default: 'device', + choices: [{ name: 'device' }, { name: 'app' }, { name: 'service' }], + message: 'Select this agent type:', + }, + ]) + await store.init({ + meta: { + name, + type, + }, + }) + + console.log('Agent is ready to use.') + } +} diff --git a/packages/access/src/cli/cmd-whoami.js b/packages/access/src/cli/cmd-whoami.js new file mode 100644 index 000000000..2526a34c4 --- /dev/null +++ b/packages/access/src/cli/cmd-whoami.js @@ -0,0 +1,33 @@ +/* eslint-disable no-console */ +import { StoreConf } from '../stores/store-conf.js' +import { NAME } from './config.js' + +/** + * @param {{ profile: any; env : string }} opts + */ +export async function cmdWhoami(opts) { + const store = new StoreConf({ profile: opts.profile }) + if (await store.exists()) { + const { delegations, meta, accounts, principal } = await store.load() + + console.log('Agent', principal.did(), meta) + console.log('Accounts:') + for (const acc of accounts) { + console.log(acc.did()) + } + + console.log('Delegations created:') + for (const created of delegations.created) { + console.log(created) + } + console.log('Delegations received:') + for (const [key, value] of delegations.receivedByResource) { + console.log( + `Resource: ${key}`, + value.map((cap) => cap.cap.can) + ) + } + } else { + console.error(`Run "${NAME} setup" first`) + } +} diff --git a/packages/access/src/cli/index.js b/packages/access/src/cli/index.js index a7b7bf84f..6c154a0a9 100755 --- a/packages/access/src/cli/index.js +++ b/packages/access/src/cli/index.js @@ -6,15 +6,19 @@ import path from 'path' import sade from 'sade' import { Transform } from 'stream' import undici from 'undici' -import { linkCmd } from './cmd-link.js' import { getConfig, NAME, pkg } from './config.js' import { getService } from './utils.js' -import inquirer from 'inquirer' // @ts-ignore // eslint-disable-next-line no-unused-vars -import * as Types from '@ucanto/interface' -import { Agent } from '../agent.js' +import { cmdCreateAccount } from './cmd-create-account.js' +import { cmdLink } from './cmd-link.js' +import { cmdSetup } from './cmd-setup.js' +import { cmdWhoami } from './cmd-whoami.js' import { StoreConf } from '../stores/store-conf.js' +import { Agent } from '../agent.js' +import inquirer from 'inquirer' +import { Verifier } from '@ucanto/principal/ed25519' +import { delegationToString, stringToDelegation } from '../encoding.js' const prog = sade(NAME) prog @@ -61,109 +65,142 @@ prog } }) -prog.command('link [channel]').describe('Link.').action(linkCmd) +prog.command('link [channel]').describe('Link.').action(cmdLink) +prog.command('setup').describe('Print config file content.').action(cmdSetup) +prog.command('whoami').describe('Print config file content.').action(cmdWhoami) +prog + .command('create-account') + .describe('Create new account.') + .action(cmdCreateAccount) prog - .command('setup') - .describe('Print config file content.') + .command('account') + .describe('Account info.') .action(async (opts) => { const store = new StoreConf({ profile: opts.profile }) - console.log('Path:', store.path) - + const { url } = await getService(opts.env) if (await store.exists()) { - console.log('Agent is already setup.') - } else { - const { name, type } = await inquirer.prompt([ - { - type: 'input', - name: 'name', - default: 'cli', - message: 'Input the name for this device:', - }, + const agent = await Agent.create({ + store, + url, + }) + + const choices = [] + for (const [key, value] of agent.data.delegations.receivedByResource) { + for (const d of value) { + if (d.cap.can === 'account/info' || d.cap.can === 'account/*') { + choices.push({ name: key }) + } + } + } + + const { account } = await inquirer.prompt([ { type: 'list', - name: 'type', + name: 'account', default: 'device', - choices: [{ name: 'device' }, { name: 'app' }, { name: 'service' }], - message: 'Select this agent type:', + choices, + message: 'Select account:', }, ]) - await store.init({ - meta: { - name, - type, - }, - }) - - console.log('Agent is ready to use.') + const result = await agent.getAccountInfo(account) + if (result.error) { + console.error(result.message) + } else { + console.log(result) + } + } else { + console.error(`Run "${NAME} setup" first`) } }) prog - .command('whoami') - .describe('Print config file content.') + .command('delegate') + .describe('Delegation capabilities.') .action(async (opts) => { const store = new StoreConf({ profile: opts.profile }) + const { url } = await getService(opts.env) if (await store.exists()) { - const { delegations, meta, accounts, agent } = await store.load() + const agent = await Agent.create({ + store, + url, + }) - console.log('Agent', agent.did(), meta) - console.log('Accounts:') - for (const acc of accounts) { - console.log(acc.did()) - } + const accountDids = agent.data.accounts.map((acc) => { + return { name: acc.did() } + }) + const { account } = await inquirer.prompt([ + { + type: 'list', + name: 'account', + choices: accountDids, + message: 'Select account:', + }, + ]) - console.log('Delegations created:') - for (const created of delegations.created) { - console.log(created) - } - console.log('Delegations received:') - for (const received of delegations.received) { - console.log(`${received.issuer.did()} -> ${received.audience.did()}`) - console.log(received.capabilities) + const abilities = [] + for (const [key, values] of agent.data.delegations.receivedByResource) { + if (key === account) { + for (const cap of values) { + abilities.push({ name: cap.cap.can }) + } + } } + const { ability } = await inquirer.prompt([ + { + type: 'list', + name: 'ability', + choices: abilities, + message: 'Select ability:', + }, + ]) + + const { audience } = await inquirer.prompt([ + { + type: 'input', + name: 'audience', + choices: abilities, + message: 'Input audience:', + }, + ]) + + console.log(account, ability) + + const delegation = await agent.delegate( + Verifier.parse(audience), + [ + { + can: ability, + with: account, + }, + ], + 800_000 + ) + + console.log(await delegationToString(delegation)) } else { console.error(`Run "${NAME} setup" first`) } }) prog - .command('create-account') - .describe('Create new account.') + .command('import') + .describe('Import delegation.') + .option('--delegation') .action(async (opts) => { - const { url } = await getService(opts.env) const store = new StoreConf({ profile: opts.profile }) - + const { url } = await getService(opts.env) if (await store.exists()) { - const spinner = ora('Registering with the service').start() const agent = await Agent.create({ store, url, }) - spinner.stopAndPersist() - const { email } = await inquirer.prompt({ - type: 'input', - name: 'email', - default: 'hugomrdias@gmail.com', - message: 'Input your email to validate:', - }) - spinner.start('Waiting for email validation...') - try { - await agent.createAccount(email) - spinner.succeed( - 'Account has been created and register with the service.' - ) - } catch (error) { - console.error(error) - // @ts-ignore - spinner.fail(error.message) - process.exit(1) - } + const del = fs.readFileSync('./delegation', { encoding: 'utf8' }) + + await agent.addDelegation(await stringToDelegation(del)) } else { - console.error('run setup command first.') - process.exit(1) + console.error(`Run "${NAME} setup" first`) } }) - prog.parse(process.argv) diff --git a/packages/access/src/delegations.js b/packages/access/src/delegations.js index 95df2da80..f893b87dc 100644 --- a/packages/access/src/delegations.js +++ b/packages/access/src/delegations.js @@ -26,6 +26,15 @@ export class Delegations { /** @type {import('./awake/types').MetaMap} */ this.meta = new Map() + + /** + * @type {Map} + */ + this.receivedByResource = new Map() + /** + * @type {Map} + */ + this.receivedMap = new Map() } /** @@ -33,16 +42,41 @@ export class Delegations { * @param {Ucanto.Delegation} delegation */ async add(delegation) { + const cid = delegation.cid.toString() + + for (const cap of delegation.capabilities) { + const byResource = this.receivedByResource.get(cap.with) ?? [] + + byResource.push({ cid: delegation.cid.toString(), cap }) + this.receivedByResource.set(cap.with, byResource) + } this.received.push(delegation) + + this.receivedMap.set(cid, delegation) + } + + /** + * @param {string} resource + */ + getByResource(resource) { + const byResource = this.receivedByResource.get(resource) + if (!byResource) { + return + } + + return byResource.map((r) => { + return this.receivedMap.get(r.cid) + }) } /** + * Add multiple received delegations * * @param {Ucanto.Delegation[]} delegations */ async addMany(delegations) { for (const d of delegations) { - this.received.push(d) + this.add(d) } } @@ -77,7 +111,8 @@ export class Delegations { audience, capabilities, lifetimeInSeconds, - proofs: this.received, + // be smarter about picking only the needs delegations + proofs: [...this.receivedMap.values()], }) this.created.push(delegation) diff --git a/packages/access/src/stores/store-conf.js b/packages/access/src/stores/store-conf.js index 61d015f08..5a30b9f74 100644 --- a/packages/access/src/stores/store-conf.js +++ b/packages/access/src/stores/store-conf.js @@ -43,7 +43,7 @@ export class StoreConf { /** @type {Store['init']} */ async init(data) { - const principal = data.agent || (await Signer.generate()) + const principal = data.principal || (await Signer.generate()) const delegations = data.delegations || new Delegations({ @@ -53,7 +53,7 @@ export class StoreConf { const storeData = { accounts: data.accounts || [], meta: data.meta || { name: 'agent', type: 'device' }, - agent: principal, + principal, delegations, } @@ -69,7 +69,7 @@ export class StoreConf { this.setAccounts(data.accounts) this.setDelegations(data.delegations) this.setMeta(data.meta) - this.setPrincipal(data.agent) + this.setPrincipal(data.principal) return this } @@ -79,7 +79,7 @@ export class StoreConf { return { accounts: await this.getAccounts(), meta: await this.getMeta(), - agent: await this.getPrincipal(), + principal: await this.getPrincipal(), delegations: await this.getDelegations(), } } @@ -139,12 +139,15 @@ export class StoreConf { const data = /** @type {DelegationsAsJSON} */ ( this.#config.get('delegations') ) - return new Delegations({ + const delegations = new Delegations({ principal: await this.getPrincipal(), created: await decodeDelegations(data.created || ''), - received: await decodeDelegations(data.received || ''), meta: new Map(data.meta), }) + + await delegations.addMany(await decodeDelegations(data.received || '')) + + return delegations } /** diff --git a/packages/access/src/stores/store-memory.js b/packages/access/src/stores/store-memory.js index 04a9d46cb..02293746e 100644 --- a/packages/access/src/stores/store-memory.js +++ b/packages/access/src/stores/store-memory.js @@ -29,7 +29,7 @@ export class StoreMemory { async close() {} async exists() { - return this.data.meta !== undefined && this.data.agent !== undefined + return this.data.meta !== undefined && this.data.principal !== undefined } static async create() { @@ -41,7 +41,7 @@ export class StoreMemory { /** @type {Store['init']} */ async init(data) { - const principal = data.agent || (await Signer.generate()) + const principal = data.principal || (await Signer.generate()) const delegations = data.delegations || new Delegations({ @@ -51,7 +51,7 @@ export class StoreMemory { const storeData = { accounts: data.accounts || [], meta: data.meta || { name: 'agent', type: 'device' }, - agent: principal, + principal, delegations, } diff --git a/packages/access/src/stores/types.ts b/packages/access/src/stores/types.ts index 8c4d23059..cd3342b9f 100644 --- a/packages/access/src/stores/types.ts +++ b/packages/access/src/stores/types.ts @@ -11,7 +11,7 @@ export interface DelegationsAsJSON { export interface StoreData { accounts: T[] meta: AgentMeta - agent: T + principal: T delegations: Delegations } diff --git a/packages/access/src/types.ts b/packages/access/src/types.ts index 17ea081d1..7d97b7cd9 100644 --- a/packages/access/src/types.ts +++ b/packages/access/src/types.ts @@ -6,9 +6,13 @@ import type { RequestEncoder, ResponseDecoder, ServiceMethod, + UCAN, + URI, } from '@ucanto/interface' import type { + AccountAll, + AccountInfo, IdentityIdentify, IdentityRegister, IdentityValidate, @@ -41,6 +45,21 @@ export interface Service { > redeem: ServiceMethod } + account: { + all: ServiceMethod + info: ServiceMethod< + AccountInfo, + { + did: UCAN.DID + agent: UCAN.DID + email: URI<'mailto:'> + product: URI<'product:'> + updated_at: string + inserted_at: string + }, + Failure + > + } } export interface AgentMeta { diff --git a/packages/store/package.json b/packages/store/package.json index 3f337436a..16ebc4dee 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -18,7 +18,7 @@ "homepage": "https://github.com/web3-storage/ucanto/tree/demo/upload-v2", "scripts": { "check": "tsc --build", - "test": "npm run test:node", + "testsss": "npm run test:node", "test:node": "mocha test", "test:browser": "pw-test test", "testw": "watch 'pnpm test' src test --interval 1",