From adb3a8d61d42b31f106e86b95faa3e442f5dc2c7 Mon Sep 17 00:00:00 2001 From: Hugo Dias Date: Mon, 5 Dec 2022 15:49:19 +0000 Subject: [PATCH] feat(access-client): cli and recover (#207) access-client cli should support #153 changes - [x] improve d1 errors .ie space already registered - [x] tests to validate that register space saves all the delegations and updates isRegistered - [x] setup cmd - [x] whoami cmd - [x] create space cmd - [x] space info - [x] delegate caps - [x] import space from delegation - [x] recover with client - [x] tests migrations actually handles it properly and keeps track of migration already applied - [x] d1 spaces table now stores metadata and the invocation that registered the space - [x] we need some names on the delegations recovered so the user can know what they are, maybe put it in the facts ? - [x] remove duplication on the on `waitForSpaceRecover` and `waitForVoucherRedeem` --- ..._add_metadata_and_invocation_to_spaces.sql | 6 + .../migrations/0002_add_delegation_column.sql | 3 + packages/access-api/package.json | 15 +- packages/access-api/readme.md | 10 + packages/access-api/scripts/migrate.js | 48 +- packages/access-api/src/bindings.d.ts | 4 + packages/access-api/src/kvs/spaces.js | 139 +++--- packages/access-api/src/service/index.js | 56 ++- .../access-api/src/service/voucher-redeem.js | 36 +- packages/access-api/src/utils/context.js | 2 +- .../access-api/test/account-recover.test.js | 189 -------- packages/access-api/test/helpers/context.js | 7 - packages/access-api/test/helpers/utils.js | 48 +- packages/access-api/test/space-info.test.js | 75 +++ .../access-api/test/space-recover.test.js | 208 ++++++++ packages/access-api/test/ucan.test.js | 444 +++++++++--------- .../access-api/test/voucher-claim.test.js | 110 +++-- .../access-api/test/voucher-redeem.test.js | 296 +++++++----- packages/access-api/tsconfig.json | 2 +- packages/access-client/package.json | 4 +- packages/access-client/src/agent.js | 157 +++++-- ...-create-account.js => cmd-create-space.js} | 28 +- packages/access-client/src/cli/cmd-whoami.js | 65 ++- packages/access-client/src/cli/index.js | 236 ++++++---- packages/access-client/src/cli/utils.js | 25 + packages/access-client/src/encoding.js | 12 + .../access-client/src/stores/store-conf.js | 7 +- packages/access-client/src/stores/types.ts | 2 +- packages/access-client/src/types.ts | 2 +- .../stores/store-indexeddb.browser.test.js | 5 +- packages/capabilities/package.json | 1 + packages/capabilities/src/index.js | 33 +- packages/capabilities/src/types.ts | 36 +- packages/upload-client/package.json | 2 +- pnpm-lock.yaml | 360 +++++++------- 35 files changed, 1587 insertions(+), 1086 deletions(-) create mode 100644 packages/access-api/migrations/0001_add_metadata_and_invocation_to_spaces.sql create mode 100644 packages/access-api/migrations/0002_add_delegation_column.sql delete mode 100644 packages/access-api/test/account-recover.test.js create mode 100644 packages/access-api/test/space-info.test.js create mode 100644 packages/access-api/test/space-recover.test.js rename packages/access-client/src/cli/{cmd-create-account.js => cmd-create-space.js} (59%) diff --git a/packages/access-api/migrations/0001_add_metadata_and_invocation_to_spaces.sql b/packages/access-api/migrations/0001_add_metadata_and_invocation_to_spaces.sql new file mode 100644 index 000000000..a4a6ed37c --- /dev/null +++ b/packages/access-api/migrations/0001_add_metadata_and_invocation_to_spaces.sql @@ -0,0 +1,6 @@ +-- Migration number: 0001 2022-11-24T11:52:58.174Z +ALTER TABLE "spaces" +ADD COLUMN "metadata" JSON NOT NULL DEFAULT '"{}"'; + +ALTER TABLE "spaces" +ADD COLUMN "invocation" text NOT NULL; \ No newline at end of file diff --git a/packages/access-api/migrations/0002_add_delegation_column.sql b/packages/access-api/migrations/0002_add_delegation_column.sql new file mode 100644 index 000000000..339d64d0b --- /dev/null +++ b/packages/access-api/migrations/0002_add_delegation_column.sql @@ -0,0 +1,3 @@ +-- Migration number: 0002 2022-11-29T14:41:37.991Z +ALTER TABLE "spaces" +ADD COLUMN "delegation" text DEFAULT NULL; \ No newline at end of file diff --git a/packages/access-api/package.json b/packages/access-api/package.json index e1d99ee5e..22feb2639 100644 --- a/packages/access-api/package.json +++ b/packages/access-api/package.json @@ -10,8 +10,8 @@ "lint": "tsc --build && eslint '**/*.{js,ts}' && prettier --check '**/*.{js,ts,yml,json}' --ignore-path ../../.gitignore", "dev": "scripts/cli.js dev", "build": "scripts/cli.js build", - "check": "tsc --build", - "test": "pnpm build && tsc --build && ava --timeout 10s" + "test": "pnpm build && mocha --bail --timeout 10s -n no-warnings -n experimental-vm-modules", + "test-watch": "pnpm build && mocha --bail --timeout 10s --watch --parallel -n no-warnings -n experimental-vm-modules --watch-files src,test" }, "author": "Hugo Dias (hugodias.me)", "license": "(Apache-2.0 OR MIT)", @@ -39,16 +39,18 @@ "@sentry/cli": "2.7.0", "@types/assert": "^1.5.6", "@types/git-rev-sync": "^2.0.0", - "@types/node": "^18.11.10", + "@types/mocha": "^10.0.1", + "@types/node": "^18.11.9", "@types/qrcode": "^1.5.0", - "ava": "^5.1.0", - "better-sqlite3": "8.0.1", + "better-sqlite3": "8.0.0", "buffer": "^6.0.3", "dotenv": "^16.0.3", "esbuild": "^0.15.16", "git-rev-sync": "^3.0.2", "hd-scripts": "^3.0.2", + "is-subset": "^0.1.1", "miniflare": "^2.11.0", + "mocha": "^10.1.0", "p-wait-for": "^5.0.0", "process": "^0.11.10", "readable-stream": "^4.2.0", @@ -66,6 +68,9 @@ "jsx": true } }, + "env": { + "mocha": true + }, "globals": { "VERSION": "readonly", "COMMITHASH": "readonly", diff --git a/packages/access-api/readme.md b/packages/access-api/readme.md index c006fdb1f..430f7bc4e 100644 --- a/packages/access-api/readme.md +++ b/packages/access-api/readme.md @@ -15,3 +15,13 @@ pnpm run lint # Run tests pnpm run test ``` + +## Migrations + +### Create migration + +```bash +pnpm exec wrangler d1 migrations create __D1_BETA__ "" +``` + +This will create a new file inside the `migrations` folder where you can write SQL. diff --git a/packages/access-api/scripts/migrate.js b/packages/access-api/scripts/migrate.js index bad869304..b15732436 100644 --- a/packages/access-api/scripts/migrate.js +++ b/packages/access-api/scripts/migrate.js @@ -2,6 +2,7 @@ import split from '@databases/split-sql-query' import sql from '@databases/sql' import path from 'path' import { fileURLToPath } from 'url' +import fs from 'fs' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -10,9 +11,12 @@ const sqliteFormat = { escapeIdentifier: (_) => '', formatValue: (_, __) => ({ placeholder: '', value: '' }), } -const migrations = [ - sql.file(`${__dirname}/../migrations/0000_create_spaces_table.sql`), -] + +// const files = globbySync(`${__dirname}/../migrations/*`) +const dir = path.resolve(`${__dirname}/../migrations`) + +const files = fs.readdirSync(dir) +const migrations = files.map((f) => sql.file(path.join(dir, f))) /** * Migrate from migration files @@ -20,24 +24,24 @@ const migrations = [ * @param {D1Database} db */ export async function migrate(db) { - try { - for (const m of migrations) { - /** @type {import('@databases/sql').SQLQuery[]} */ - // @ts-ignore - const qs = split.default(m) - await db.batch( - qs.map((q) => { - return db.prepare(q.format(sqliteFormat).text.replace(/^--.*$/gm, '')) - }) - ) - } - } catch (error) { - const err = /** @type {Error} */ (error) - // eslint-disable-next-line no-console - console.error('D1 Error', { - message: err.message, - // @ts-ignore - cause: err.cause?.message, - }) + const appliedMigrations = /** @type {number} */ ( + await db.prepare('PRAGMA user_version').first('user_version') + ) + + migrations.splice(0, appliedMigrations) + const remaining = migrations.length + for (const m of migrations) { + /** @type {import('@databases/sql').SQLQuery[]} */ + // @ts-ignore + const qs = split.default(m) + await db.batch( + qs.map((q) => { + return db.prepare(q.format(sqliteFormat).text.replace(/^--.*$/gm, '')) + }) + ) + + await db + .prepare(`PRAGMA user_version = ${appliedMigrations + remaining}`) + .all() } } diff --git a/packages/access-api/src/bindings.d.ts b/packages/access-api/src/bindings.d.ts index 5a99c4bb5..50adbe61c 100644 --- a/packages/access-api/src/bindings.d.ts +++ b/packages/access-api/src/bindings.d.ts @@ -78,3 +78,7 @@ export interface ModuleWorker { fetch?: ModuleWorker.FetchHandler scheduled?: ModuleWorker.CronHandler } + +export interface D1ErrorRaw extends Error { + cause: Error & { code: string } +} diff --git a/packages/access-api/src/kvs/spaces.js b/packages/access-api/src/kvs/spaces.js index c54fdfc5a..e99c6c4c0 100644 --- a/packages/access-api/src/kvs/spaces.js +++ b/packages/access-api/src/kvs/spaces.js @@ -1,40 +1,77 @@ // @ts-ignore // eslint-disable-next-line no-unused-vars import * as Ucanto from '@ucanto/interface' -import { delegationToString } from '@web3-storage/access/encoding' +import { + delegationToString, + stringToDelegation, +} from '@web3-storage/access/encoding' /** * @typedef {import('@web3-storage/access/types').SpaceD1} SpaceD1 */ +/** + * @implements {Ucanto.Failure} + */ +export class D1Error extends Error { + /** @type {true} */ + get error() { + return true + } + + /** + * + * @param {import('../bindings').D1ErrorRaw} error + */ + constructor(error) { + super(`${error.cause.message} (${error.cause.code})`, { + cause: error.cause, + }) + this.name = 'D1Error' + this.code = error.cause.code + } +} + /** * Spaces */ export class Spaces { /** * - * @param {KVNamespace} kv * @param {import('workers-qb').D1QB} db */ - constructor(kv, db) { - this.kv = kv + constructor(db) { this.db = db } /** * @param {import('@web3-storage/capabilities/types').VoucherRedeem} capability * @param {Ucanto.Invocation} invocation + * @param {Ucanto.Delegation<[import('@web3-storage/access/src/types').Top]> | undefined} delegation */ - async create(capability, invocation) { - await this.db.insert({ - tableName: 'spaces', - data: { - did: capability.nb.space, - product: capability.nb.product, - email: capability.nb.identity.replace('mailto:', ''), - agent: invocation.issuer.did(), - }, - }) + async create(capability, invocation, delegation) { + try { + const result = await this.db.insert({ + tableName: 'spaces', + data: { + did: capability.nb.space, + product: capability.nb.product, + email: capability.nb.identity.replace('mailto:', ''), + agent: invocation.issuer.did(), + metadata: JSON.stringify(invocation.facts[0]), + invocation: await delegationToString(invocation), + // eslint-disable-next-line unicorn/no-null + delegation: !delegation ? null : await delegationToString(delegation), + }, + }) + return { data: result } + } catch (error) { + return { + error: new D1Error( + /** @type {import('../bindings').D1ErrorRaw} */ (error) + ), + } + } } /** @@ -63,55 +100,49 @@ export class Spaces { product: results.product, updated_at: results.update_at, inserted_at: results.inserted_at, + // @ts-ignore + metadata: JSON.parse(results.metadata), }) } /** - * Save space delegation per email - * - * @param {`mailto:${string}`} email - * @param {Ucanto.Delegation} delegation + * @param {string} email */ - async saveDelegation(email, delegation) { - const accs = /** @type {string[] | undefined} */ ( - await this.kv.get(email, { - type: 'json', - }) - ) + async getByEmail(email) { + const s = await this.db.fetchAll({ + tableName: 'spaces', + fields: '*', + where: { + conditions: 'email=?1', + params: [email], + }, + }) - if (accs) { - accs.push(await delegationToString(delegation)) - await this.kv.put(email, JSON.stringify(accs)) - } else { - await this.kv.put( - email, - JSON.stringify([await delegationToString(delegation)]) - ) + if (!s.results || s.results.length === 0) { + return } - } - /** - * Check if we have delegations for an email - * - * @param {`mailto:${string}`} email - */ - async hasDelegations(email) { - const r = await this.kv.get(email) - return Boolean(r) - } - - /** - * @param {`mailto:${string}`} email - */ - async getDelegations(email) { - const r = await this.kv.get(email, { type: 'json' }) + const out = [] - if (!r) { - return + for (const r of s.results) { + out.push({ + did: r.did, + agent: r.agent, + email: r.email, + product: r.product, + updated_at: r.update_at, + inserted_at: r.inserted_at, + // @ts-ignore + metadata: JSON.parse(r.metadata), + delegation: !r.delegation + ? undefined + : await stringToDelegation( + /** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/access/types').Top]>} */ ( + r.delegation + ) + ), + }) } - - return /** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/capabilities/types').Top]>[]} */ ( - r - ) + return out } } diff --git a/packages/access-api/src/service/index.js b/packages/access-api/src/service/index.js index 93474f634..47d374cce 100644 --- a/packages/access-api/src/service/index.js +++ b/packages/access-api/src/service/index.js @@ -1,13 +1,11 @@ +import * as DID from '@ipld/dag-ucan/did' import * as Server from '@ucanto/server' import { Failure } from '@ucanto/server' import * as Space from '@web3-storage/capabilities/space' +import { top } from '@web3-storage/capabilities/top' +import { delegationToString } from '@web3-storage/access/encoding' import { voucherClaimProvider } from './voucher-claim.js' import { voucherRedeemProvider } from './voucher-redeem.js' -import * as DID from '@ipld/dag-ucan/did' -import { - delegationToString, - stringToDelegation, -} from '@web3-storage/access/encoding' /** * @param {import('../bindings').RouteContext} ctx @@ -35,31 +33,33 @@ export function service(ctx) { return new Failure( `Resource ${ capability.with - } does not service did ${ctx.signer.did()}` + } does not match service did ${ctx.signer.did()}` ) } - const encoded = await ctx.kvs.spaces.getDelegations( - capability.nb.identity + const spaces = await ctx.kvs.spaces.getByEmail( + capability.nb.identity.replace('mailto:', '') ) - if (!encoded) { + if (!spaces) { return new Failure( `No delegations found for ${capability.nb.identity}` ) } const results = [] - for (const e of encoded) { - const proof = await stringToDelegation(e) - const del = await Space.top.delegate({ - audience: invocation.issuer, - issuer: ctx.signer, - with: proof.capabilities[0].with, - expiration: Infinity, - proofs: [proof], - }) + for (const { delegation, metadata } of spaces) { + if (delegation) { + const del = await top.delegate({ + audience: invocation.issuer, + issuer: ctx.signer, + with: delegation.capabilities[0].with, + expiration: Infinity, + proofs: [delegation], + facts: [metadata], + }) - results.push(await delegationToString(del)) + results.push(await delegationToString(del)) + } } return results @@ -73,10 +73,15 @@ export function service(ctx) { // if yes send email with space/recover // if not error "no spaces for email X" - const email = capability.nb.identity - if (!(await ctx.kvs.spaces.hasDelegations(email))) { + const spaces = await ctx.kvs.spaces.getByEmail( + capability.nb.identity.replace('mailto:', '') + ) + if (!spaces) { return new Failure( - `No spaces found for email: ${email.replace('mailto:', '')}.` + `No spaces found for email: ${capability.nb.identity.replace( + 'mailto:', + '' + )}.` ) } @@ -87,7 +92,7 @@ export function service(ctx) { with: ctx.signer.did(), lifetimeInSeconds: 60 * 10, nb: { - identity: email, + identity: capability.nb.identity, }, proofs: [ await Space.recover.delegate({ @@ -110,6 +115,11 @@ export function service(ctx) { if (ctx.config.ENV === 'test') { return url } + + await ctx.email.sendValidation({ + to: capability.nb.identity.replace('mailto:', ''), + url, + }) } ), }, diff --git a/packages/access-api/src/service/voucher-redeem.js b/packages/access-api/src/service/voucher-redeem.js index 20d6a098d..8a8065495 100644 --- a/packages/access-api/src/service/voucher-redeem.js +++ b/packages/access-api/src/service/voucher-redeem.js @@ -1,3 +1,6 @@ +// @ts-ignore +// eslint-disable-next-line no-unused-vars +import * as Ucanto from '@ucanto/interface' import * as Server from '@ucanto/server' import * as Voucher from '@web3-storage/capabilities/voucher' import { Delegation } from '@ucanto/core' @@ -9,24 +12,47 @@ export function voucherRedeemProvider(ctx) { return Server.provide(Voucher.redeem, async ({ capability, invocation }) => { if (capability.with !== ctx.signer.did()) { return new Failure( - `Resource ${capability.with} does not service did ${ctx.signer.did()}` + `Resource ${ + capability.with + } does not match service did ${ctx.signer.did()}` ) } - // @ts-ignore - TODO fix this - await ctx.kvs.spaces.create(capability, invocation) + /** @type {Ucanto.Delegation[]} */ + const delegations = [] // We should only save delegation for email identities if (capability.nb.identity.startsWith('mailto:')) { for (const p of invocation.proofs) { if ( Delegation.isDelegation(p) && - p.audience.did() === ctx.signer.did() + p.audience.did() === ctx.signer.did() && + p.capabilities[0].with === capability.nb.space && + p.capabilities[0].can === '*' ) { - await ctx.kvs.spaces.saveDelegation(capability.nb.identity, p) + delegations.push(p) } } } + if (delegations.length > 1) { + return new Failure('Multiple space delegations not suppported.') + } + + const { error } = await ctx.kvs.spaces.create( + capability, + // @ts-ignore - TODO fix this + invocation, + delegations[0] + ) + + if (error) { + if (error.code === 'SQLITE_CONSTRAINT_PRIMARYKEY') { + return new Failure(`Space ${capability.nb.space} already registered.`) + } else { + throw error + } + } + ctx.config.METRICS.writeDataPoint({ blobs: [ctx.config.ENV, 'new_space_v1'], doubles: [1], diff --git a/packages/access-api/src/utils/context.js b/packages/access-api/src/utils/context.js index 235ea2068..747889c57 100644 --- a/packages/access-api/src/utils/context.js +++ b/packages/access-api/src/utils/context.js @@ -51,7 +51,7 @@ export function getContext(request, env, ctx) { config, url, kvs: { - spaces: new Spaces(config.SPACES, db), + spaces: new Spaces(db), validations: new Validations(config.VALIDATIONS), }, email: new Email({ token: config.POSTMARK_TOKEN }), diff --git a/packages/access-api/test/account-recover.test.js b/packages/access-api/test/account-recover.test.js deleted file mode 100644 index 7145c09f1..000000000 --- a/packages/access-api/test/account-recover.test.js +++ /dev/null @@ -1,189 +0,0 @@ -import * as Space from '@web3-storage/capabilities/space' -import { stringToDelegation } from '@web3-storage/access/encoding' -import pWaitFor from 'p-wait-for' -import { context, test } from './helpers/context.js' -import { Validations } from '../src/kvs/validations.js' - -import { createSpace } from './helpers/utils.js' - -test.beforeEach(async (t) => { - t.context = await context() -}) - -test('should fail before registering space', async (t) => { - const { issuer, service, conn } = t.context - - const inv = await Space.recoverValidation - .invoke({ - issuer, - audience: service, - with: issuer.did(), - nb: { - identity: 'mailto:hello@dag.house', - }, - }) - .execute(conn) - - if (inv?.error) { - t.deepEqual(inv.message, `No spaces found for email: hello@dag.house.`) - } else { - return t.fail() - } -}) - -test('should return space/recover', async (t) => { - const { issuer, service, conn, mf } = t.context - - await createSpace(issuer, service, conn, 'space-recover@dag.house') - - const inv = await Space.recoverValidation - .invoke({ - issuer, - audience: service, - with: issuer.did(), - nb: { - identity: 'mailto:space-recover@dag.house', - }, - }) - .execute(conn) - - if (!inv || inv.error) { - return t.fail('failed to recover') - } - - const url = new URL(inv) - const encoded = - /** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/capabilities/types').SpaceRecover]>} */ ( - url.searchParams.get('ucan') - ) - - const del = await stringToDelegation(encoded) - - t.deepEqual(del.audience.did(), issuer.did()) - t.deepEqual(del.issuer.did(), service.did()) - t.deepEqual(del.capabilities[0].can, 'space/recover') - const rsp = await mf.dispatchFetch(url) - const html = await rsp.text() - - t.assert(html.includes(encoded)) - - // @ts-ignore - const validations = new Validations(await mf.getKVNamespace('VALIDATIONS')) - const recoverEncoded = - /** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/capabilities/types').SpaceRecover]>} */ ( - await validations.get(issuer.did()) - ) - - t.truthy(recoverEncoded) - const recover = await stringToDelegation(recoverEncoded) - t.deepEqual(recover.audience.did(), issuer.did()) - t.deepEqual(recover.issuer.did(), service.did()) - t.deepEqual(recover.capabilities[0].can, 'space/recover') - - // ws - const res = await mf.dispatchFetch('http://localhost:8787/validate-ws', { - headers: { Upgrade: 'websocket' }, - }) - - const webSocket = res.webSocket - if (webSocket) { - let done = false - webSocket.accept() - webSocket.addEventListener('message', async (event) => { - // @ts-ignore - const data = JSON.parse(event.data) - - const encoded = - /** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/capabilities/types').SpaceRecover]>} */ ( - data.delegation - ) - - t.truthy(encoded) - const recover = await stringToDelegation(encoded) - t.deepEqual(recover.audience.did(), issuer.did()) - t.deepEqual(recover.issuer.did(), service.did()) - t.deepEqual(recover.capabilities[0].can, 'space/recover') - done = true - }) - - webSocket.send( - JSON.stringify({ - did: issuer.did(), - }) - ) - - await pWaitFor(() => done) - } else { - t.fail('should have ws') - } -}) - -test('should invoke space/recover and get space delegation', async (t) => { - const { issuer, service, conn } = t.context - const email = 'space-recover@dag.house' - const { space } = await createSpace(issuer, service, conn, email) - - const inv = await Space.recoverValidation - .invoke({ - issuer, - audience: service, - with: issuer.did(), - nb: { - // @ts-ignore - identity: `mailto:${email}`, - }, - }) - .execute(conn) - - if (!inv || inv.error) { - return t.fail('failed to recover') - } - - const url = new URL(inv) - const encoded = - /** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/capabilities/types').SpaceRecover]>} */ ( - url.searchParams.get('ucan') - ) - - const del = await stringToDelegation(encoded) - - t.deepEqual(del.audience.did(), issuer.did()) - t.deepEqual(del.issuer.did(), service.did()) - t.deepEqual(del.capabilities[0].can, 'space/recover') - - const inv2 = await Space.recover - .invoke({ - issuer, - audience: service, - with: service.did(), - nb: { - identity: del.capabilities[0].nb.identity, - }, - proofs: [del], - }) - .execute(conn) - - if (!inv2 || inv2.error) { - return t.fail('failed to recover') - } - - const spaceDelegation = await stringToDelegation(inv2[0]) - t.deepEqual(spaceDelegation.audience.did(), issuer.did()) - t.deepEqual(spaceDelegation.capabilities[0].can, '*') - t.deepEqual(spaceDelegation.capabilities[0].with, space.did()) - - const spaceInfo = await Space.info - .invoke({ - issuer, - audience: service, - with: space.did(), - proofs: [spaceDelegation], - }) - .execute(conn) - - if (!spaceInfo || spaceInfo.error) { - return t.fail('failed to get space info') - } - - t.deepEqual(spaceInfo.did, space.did()) -}) diff --git a/packages/access-api/test/helpers/context.js b/packages/access-api/test/helpers/context.js index 479cb4ab9..f190e0b6c 100644 --- a/packages/access-api/test/helpers/context.js +++ b/packages/access-api/test/helpers/context.js @@ -1,7 +1,6 @@ /* eslint-disable no-console */ import { Signer } from '@ucanto/principal/ed25519' import { connection } from '@web3-storage/access' -import anyTest from 'ava' import dotenv from 'dotenv' import { Miniflare } from 'miniflare' import path from 'path' @@ -14,12 +13,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) dotenv.config({ path: path.join(__dirname, '..', '..', '..', '..', '.env.tpl'), }) -/** - * @typedef {import("ava").TestFn>>} TestFn - */ - -// eslint-disable-next-line unicorn/prefer-export-from -export const test = /** @type {TestFn} */ (anyTest) export const bindings = { ENV: 'test', diff --git a/packages/access-api/test/helpers/utils.js b/packages/access-api/test/helpers/utils.js index e0926c13b..3fec6a7e2 100644 --- a/packages/access-api/test/helpers/utils.js +++ b/packages/access-api/test/helpers/utils.js @@ -27,6 +27,12 @@ export async function send(ucan, mf) { */ export async function createSpace(issuer, service, conn, email) { const space = await Signer.generate() + const spaceDelegation = await Voucher.top.delegate({ + issuer: space, + audience: issuer, + with: space.did(), + expiration: Infinity, + }) const claim = await Voucher.claim .invoke({ issuer, @@ -38,14 +44,7 @@ export async function createSpace(issuer, service, conn, email) { product: 'product:free', service: service.did(), }, - proofs: [ - await Voucher.top.delegate({ - issuer: space, - audience: issuer, - with: space.did(), - expiration: Infinity, - }), - ], + proofs: [spaceDelegation], }) .execute(conn) if (!claim || claim.error) { @@ -53,7 +52,12 @@ export async function createSpace(issuer, service, conn, email) { } const delegation = await stringToDelegation(claim) - + const serviceDelegation = await Voucher.top.delegate({ + issuer: space, + audience: service, + with: space.did(), + expiration: Infinity, + }) const redeem = await Voucher.redeem .invoke({ issuer, @@ -64,25 +68,33 @@ export async function createSpace(issuer, service, conn, email) { identity: delegation.capabilities[0].nb.identity, product: delegation.capabilities[0].nb.product, }, - proofs: [ - delegation, - await Voucher.top.delegate({ - issuer: space, - audience: service, - with: space.did(), - expiration: Infinity, - }), + facts: [ + { + space: { + name: `name-${email}`, + }, + agent: { + name: 'testing-agent', + type: 'device', + description: 'testing', + url: 'https://dag.house', + image: 'https://dag.house/logo.jpg', + }, + }, ], + + proofs: [delegation, serviceDelegation], }) .execute(conn) if (redeem?.error) { // eslint-disable-next-line no-console - console.log(redeem) + console.log('create space util error', redeem) throw new Error(redeem.message) } return { space, + delegation: spaceDelegation, } } diff --git a/packages/access-api/test/space-info.test.js b/packages/access-api/test/space-info.test.js new file mode 100644 index 000000000..85a6b5cf0 --- /dev/null +++ b/packages/access-api/test/space-info.test.js @@ -0,0 +1,75 @@ +import * as Space from '@web3-storage/capabilities/space' +import assert from 'assert' +import { context } from './helpers/context.js' +import { createSpace } from './helpers/utils.js' +// @ts-ignore +import isSubset from 'is-subset' + +describe('space/info', function () { + /** @type {Awaited>} */ + let ctx + beforeEach(async function () { + ctx = await context() + }) + + it('should fail before registering space', async function () { + const { issuer, service, conn } = ctx + + const inv = await Space.info + .invoke({ + issuer, + audience: service, + with: issuer.did(), + }) + .execute(conn) + + if (inv?.error) { + assert.deepEqual(inv.message, `Space not found.`) + } else { + assert.fail() + } + }) + + it('should return space info', async function () { + const { issuer, service, conn } = ctx + + const { space, delegation } = await createSpace( + issuer, + service, + conn, + 'space-info@dag.house' + ) + + const inv = await Space.info + .invoke({ + issuer, + audience: service, + with: space.did(), + proofs: [delegation], + }) + .execute(conn) + + if (inv?.error) { + assert.fail() + } else { + assert.ok( + isSubset(inv, { + did: space.did(), + agent: issuer.did(), + email: 'space-info@dag.house', + product: 'product:free', + metadata: { + space: { name: 'name-space-info@dag.house' }, + agent: { + url: 'https://dag.house', + name: 'testing-agent', + type: 'device', + image: 'https://dag.house/logo.jpg', + description: 'testing', + }, + }, + }) + ) + } + }) +}) diff --git a/packages/access-api/test/space-recover.test.js b/packages/access-api/test/space-recover.test.js new file mode 100644 index 000000000..99b80315b --- /dev/null +++ b/packages/access-api/test/space-recover.test.js @@ -0,0 +1,208 @@ +import * as Space from '@web3-storage/capabilities/space' +import { stringToDelegation } from '@web3-storage/access/encoding' +import pWaitFor from 'p-wait-for' +import assert from 'assert' +import { context } from './helpers/context.js' +import { Validations } from '../src/kvs/validations.js' +import { createSpace } from './helpers/utils.js' + +describe('space-recover', function () { + /** @type {Awaited>} */ + let ctx + beforeEach(async function () { + ctx = await context() + }) + + it('should fail before registering space', async function () { + const { issuer, service, conn } = ctx + + const inv = await Space.recoverValidation + .invoke({ + issuer, + audience: service, + with: issuer.did(), + nb: { + identity: 'mailto:hello@dag.house', + }, + }) + .execute(conn) + + if (inv?.error) { + assert.deepEqual( + inv.message, + `No spaces found for email: hello@dag.house.` + ) + } else { + assert.fail() + } + }) + + it('should return space/recover', async function () { + const { issuer, service, conn, mf } = ctx + + await createSpace(issuer, service, conn, 'space-recover@dag.house') + + const inv = await Space.recoverValidation + .invoke({ + issuer, + audience: service, + with: issuer.did(), + nb: { + identity: 'mailto:space-recover@dag.house', + }, + }) + .execute(conn) + + if (!inv || inv.error) { + return assert.fail('failed to recover') + } + + const url = new URL(inv) + const encoded = + /** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/capabilities/types').SpaceRecover]>} */ ( + url.searchParams.get('ucan') + ) + + const del = await stringToDelegation(encoded) + + assert.deepEqual(del.audience.did(), issuer.did()) + assert.deepEqual(del.issuer.did(), service.did()) + assert.deepEqual(del.capabilities[0].can, 'space/recover') + const rsp = await mf.dispatchFetch(url) + const html = await rsp.text() + + assert(html.includes(encoded)) + + // @ts-ignore + const validations = new Validations(await mf.getKVNamespace('VALIDATIONS')) + const recoverEncoded = + /** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/capabilities/types').SpaceRecover]>} */ ( + await validations.get(issuer.did()) + ) + + assert.ok(recoverEncoded) + const recover = await stringToDelegation(recoverEncoded) + assert.deepEqual(recover.audience.did(), issuer.did()) + assert.deepEqual(recover.issuer.did(), service.did()) + assert.deepEqual(recover.capabilities[0].can, 'space/recover') + + // ws + const res = await mf.dispatchFetch('http://localhost:8787/validate-ws', { + headers: { Upgrade: 'websocket' }, + }) + + const webSocket = res.webSocket + if (webSocket) { + let done = false + webSocket.accept() + webSocket.addEventListener('message', async (event) => { + // @ts-ignore + const data = JSON.parse(event.data) + + const encoded = + /** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/capabilities/types').SpaceRecover]>} */ ( + data.delegation + ) + + assert.ok(encoded) + const recover = await stringToDelegation(encoded) + assert.deepEqual(recover.audience.did(), issuer.did()) + assert.deepEqual(recover.issuer.did(), service.did()) + assert.deepEqual(recover.capabilities[0].can, 'space/recover') + done = true + }) + + webSocket.send( + JSON.stringify({ + did: issuer.did(), + }) + ) + + await pWaitFor(() => done) + } else { + assert.fail('should have ws') + } + }) + + it('should invoke space/recover and get space delegation', async function () { + const { issuer, service, conn } = ctx + const email = 'space-recover@dag.house' + const { space } = await createSpace(issuer, service, conn, email) + + const inv = await Space.recoverValidation + .invoke({ + issuer, + audience: service, + with: issuer.did(), + nb: { + // @ts-ignore + identity: `mailto:${email}`, + }, + }) + .execute(conn) + + if (!inv || inv.error) { + return assert.fail('failed to recover') + } + + const url = new URL(inv) + const encoded = + /** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/capabilities/types').SpaceRecover]>} */ ( + url.searchParams.get('ucan') + ) + + const del = await stringToDelegation(encoded) + + assert.deepEqual(del.audience.did(), issuer.did()) + assert.deepEqual(del.issuer.did(), service.did()) + assert.deepEqual(del.capabilities[0].can, 'space/recover') + + const inv2 = await Space.recover + .invoke({ + issuer, + audience: service, + with: service.did(), + nb: { + identity: del.capabilities[0].nb.identity, + }, + proofs: [del], + }) + .execute(conn) + + if (!inv2 || inv2.error) { + return assert.fail('failed to recover') + } + + const spaceDelegation = await stringToDelegation(inv2[0]) + assert.deepEqual(spaceDelegation.audience.did(), issuer.did()) + assert.deepEqual(spaceDelegation.capabilities[0].can, '*') + assert.deepEqual(spaceDelegation.capabilities[0].with, space.did()) + assert.deepEqual(spaceDelegation.facts[0], { + agent: { + description: 'testing', + image: 'https://dag.house/logo.jpg', + name: 'testing-agent', + type: 'device', + url: 'https://dag.house', + }, + space: { + name: 'name-' + email, + }, + }) + + const spaceInfo = await Space.info + .invoke({ + issuer, + audience: service, + with: space.did(), + proofs: [spaceDelegation], + }) + .execute(conn) + + if (!spaceInfo || spaceInfo.error) { + return assert.fail('failed to get space info') + } + + assert.deepEqual(spaceInfo.did, space.did()) + }) +}) diff --git a/packages/access-api/test/ucan.test.js b/packages/access-api/test/ucan.test.js index 2d34e1176..09d7a9dc8 100644 --- a/packages/access-api/test/ucan.test.js +++ b/packages/access-api/test/ucan.test.js @@ -1,242 +1,250 @@ import * as UCAN from '@ipld/dag-ucan' import { Signer } from '@ucanto/principal/ed25519' -import { context, test } from './helpers/context.js' - -test.beforeEach(async (t) => { - t.context = await context() -}) - -test('should fail with no header', async (t) => { - const { mf } = t.context - const res = await mf.dispatchFetch('http://localhost:8787/raw', { - method: 'POST', - }) - const rsp = await res.json() - t.deepEqual(rsp, { - error: { - code: 'HTTP_ERROR', - message: 'The required "Authorization: Bearer" header is missing.', - }, +import { context } from './helpers/context.js' +import assert from 'assert' + +/** @type {typeof assert} */ +const t = assert +const test = it + +describe('ucan', function () { + /** @type {Awaited>} */ + let ctx + beforeEach(async function () { + ctx = await context() + }) + it('should fail with no header', async function () { + const { mf } = ctx + const res = await mf.dispatchFetch('http://localhost:8787/raw', { + method: 'POST', + }) + const rsp = await res.json() + t.deepEqual(rsp, { + error: { + code: 'HTTP_ERROR', + message: 'The required "Authorization: Bearer" header is missing.', + }, + }) + t.strictEqual(res.status, 400) }) - t.is(res.status, 400) -}) -test('should fail with bad ucan', async (t) => { - const { mf } = t.context + it('should fail with bad ucan', async function () { + const { mf } = ctx - const res = await mf.dispatchFetch('http://localhost:8787/raw', { - method: 'POST', - headers: { - Authorization: `Bearer ss`, - }, - }) - t.is(res.status, 401) - const rsp = await res.json() - t.deepEqual(rsp, { - error: { - code: 'HTTP_ERROR', - message: 'Malformed UCAN headers data.', - cause: - "ParseError: Can't parse UCAN: ss: Expected JWT format: 3 dot-separated base64url-encoded values.", - }, + const res = await mf.dispatchFetch('http://localhost:8787/raw', { + method: 'POST', + headers: { + Authorization: `Bearer ss`, + }, + }) + t.strictEqual(res.status, 401) + const rsp = await res.json() + t.deepEqual(rsp, { + error: { + code: 'HTTP_ERROR', + message: 'Malformed UCAN headers data.', + cause: + "ParseError: Can't parse UCAN: ss: Expected JWT format: 3 dot-separated base64url-encoded values.", + }, + }) }) -}) -test('should fail with 0 caps', async (t) => { - const { mf, service, issuer } = t.context + test('should fail with 0 caps', async function () { + const { mf, service, issuer } = ctx - const ucan = await UCAN.issue({ - issuer, - audience: service, - // @ts-ignore - capabilities: [], - }) - const res = await mf.dispatchFetch('http://localhost:8787/raw', { - method: 'POST', - headers: { - Authorization: `Bearer ${UCAN.format(ucan)}`, - }, - }) - const rsp = await res.json() - t.deepEqual(rsp, [ - { - name: 'InvocationCapabilityError', - error: true, - message: 'Invocation is required to have a single capability.', + const ucan = await UCAN.issue({ + issuer, + audience: service, + // @ts-ignore capabilities: [], - }, - ]) -}) + }) + const res = await mf.dispatchFetch('http://localhost:8787/raw', { + method: 'POST', + headers: { + Authorization: `Bearer ${UCAN.format(ucan)}`, + }, + }) + const rsp = await res.json() + t.deepEqual(rsp, [ + { + name: 'InvocationCapabilityError', + error: true, + message: 'Invocation is required to have a single capability.', + capabilities: [], + }, + ]) + }) -test('should fail with bad service audience', async (t) => { - const { mf, issuer } = t.context + test('should fail with bad service audience', async function () { + const { mf, issuer } = ctx - const audience = await Signer.generate() - const ucan = await UCAN.issue({ - issuer, - audience, - // @ts-ignore - capabilities: [], - }) - const res = await mf.dispatchFetch('http://localhost:8787/raw', { - method: 'POST', - headers: { - Authorization: `Bearer ${UCAN.format(ucan)}`, - }, + const audience = await Signer.generate() + const ucan = await UCAN.issue({ + issuer, + audience, + // @ts-ignore + capabilities: [], + }) + const res = await mf.dispatchFetch('http://localhost:8787/raw', { + method: 'POST', + headers: { + Authorization: `Bearer ${UCAN.format(ucan)}`, + }, + }) + const rsp = await res.json() + t.deepEqual(rsp[0].name, 'InvalidAudience') }) - const rsp = await res.json() - t.deepEqual(rsp[0].name, 'InvalidAudience') -}) -test('should fail with with more than 1 cap', async (t) => { - const { mf, service, issuer } = t.context + test('should fail with with more than 1 cap', async function () { + const { mf, service, issuer } = ctx - const ucan = await UCAN.issue({ - issuer, - audience: service, - capabilities: [ - { can: 'identity/validate', with: 'mailto:admin@dag.house' }, - { can: 'identity/register', with: 'mailto:admin@dag.house' }, - ], - }) - const res = await mf.dispatchFetch('http://localhost:8787/raw', { - method: 'POST', - headers: { - Authorization: `Bearer ${UCAN.format(ucan)}`, - }, - }) - const rsp = await res.json() - t.deepEqual(rsp, [ - { - name: 'InvocationCapabilityError', - error: true, - message: 'Invocation is required to have a single capability.', + const ucan = await UCAN.issue({ + issuer, + audience: service, capabilities: [ { can: 'identity/validate', with: 'mailto:admin@dag.house' }, { can: 'identity/register', with: 'mailto:admin@dag.house' }, ], - }, - ]) -}) - -test('should route to handler', async (t) => { - const { mf, issuer, service } = t.context - - const ucan = await UCAN.issue({ - issuer, - audience: service, - capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], - }) - const res = await mf.dispatchFetch('http://localhost:8787/raw', { - method: 'POST', - headers: { - Authorization: `Bearer ${UCAN.format(ucan)}`, - }, - }) - const rsp = await res.json() - t.deepEqual(rsp, ['test pass']) -}) - -test('should handle exception in route handler', async (t) => { - const { mf, service, issuer } = t.context - - const ucan = await UCAN.issue({ - issuer, - audience: service, - capabilities: [{ can: 'testing/fail', with: 'mailto:admin@dag.house' }], - }) - const res = await mf.dispatchFetch('http://localhost:8787/raw', { - method: 'POST', - headers: { - Authorization: `Bearer ${UCAN.format(ucan)}`, - }, - }) - const rsp = await res.json() - t.deepEqual( - rsp[0].message, - 'service handler {can: "testing/fail"} error: test fail' - ) -}) - -test('should fail with missing proofs', async (t) => { - const { mf, service } = t.context - - const alice = await Signer.generate() - const bob = await Signer.generate() - const proof1 = await UCAN.issue({ - issuer: alice, - audience: bob, - capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], - }) - - const proof2 = await UCAN.issue({ - issuer: alice, - audience: bob, - capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], - }) - const cid1 = await UCAN.link(proof1) - const cid2 = await UCAN.link(proof2) - const ucan = await UCAN.issue({ - issuer: bob, - audience: service, - capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], - proofs: [cid1, cid2], - }) - - const res = await mf.dispatchFetch('http://localhost:8787/raw', { - method: 'POST', - headers: { - Authorization: `Bearer ${UCAN.format(ucan)}`, - }, - }) - const rsp = await res.json() - t.deepEqual(rsp, { - error: { - code: 'HTTP_ERROR', - message: 'Missing Proofs', - cause: { - prf: [cid1.toString(), cid2.toString()], + }) + const res = await mf.dispatchFetch('http://localhost:8787/raw', { + method: 'POST', + headers: { + Authorization: `Bearer ${UCAN.format(ucan)}`, }, - }, - }) -}) - -test('should multiple invocation should pass', async (t) => { - const { mf, service } = t.context - - const alice = await Signer.generate() - const bob = await Signer.generate() - const proof1 = await UCAN.issue({ - issuer: alice, - audience: bob, - capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], - }) - - const cid1 = await UCAN.link(proof1) - const ucan1 = await UCAN.issue({ - issuer: bob, - audience: service, - capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], - proofs: [cid1], - }) - - const ucan2 = await UCAN.issue({ - issuer: bob, - audience: service, - capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], - proofs: [cid1], + }) + const rsp = await res.json() + t.deepEqual(rsp, [ + { + name: 'InvocationCapabilityError', + error: true, + message: 'Invocation is required to have a single capability.', + capabilities: [ + { can: 'identity/validate', with: 'mailto:admin@dag.house' }, + { can: 'identity/register', with: 'mailto:admin@dag.house' }, + ], + }, + ]) }) - const headers = new Headers() - headers.append('Authorization', `Bearer ${UCAN.format(ucan1)}`) - headers.append('Authorization', `Bearer ${UCAN.format(ucan2)}`) - headers.append('ucan', `${cid1.toString()} ${UCAN.format(proof1)}`) + test('should route to handler', async function () { + const { mf, issuer, service } = ctx - const res = await mf.dispatchFetch('http://localhost:8787/raw', { - method: 'POST', - headers: Object.fromEntries(headers.entries()), + const ucan = await UCAN.issue({ + issuer, + audience: service, + capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], + }) + const res = await mf.dispatchFetch('http://localhost:8787/raw', { + method: 'POST', + headers: { + Authorization: `Bearer ${UCAN.format(ucan)}`, + }, + }) + const rsp = await res.json() + t.deepEqual(rsp, ['test pass']) + }) + + test('should handle exception in route handler', async function () { + const { mf, service, issuer } = ctx + + const ucan = await UCAN.issue({ + issuer, + audience: service, + capabilities: [{ can: 'testing/fail', with: 'mailto:admin@dag.house' }], + }) + const res = await mf.dispatchFetch('http://localhost:8787/raw', { + method: 'POST', + headers: { + Authorization: `Bearer ${UCAN.format(ucan)}`, + }, + }) + const rsp = await res.json() + t.deepEqual( + rsp[0].message, + 'service handler {can: "testing/fail"} error: test fail' + ) + }) + + test('should fail with missing proofs', async function () { + const { mf, service } = ctx + + const alice = await Signer.generate() + const bob = await Signer.generate() + const proof1 = await UCAN.issue({ + issuer: alice, + audience: bob, + capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], + }) + + const proof2 = await UCAN.issue({ + issuer: alice, + audience: bob, + capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], + }) + const cid1 = await UCAN.link(proof1) + const cid2 = await UCAN.link(proof2) + const ucan = await UCAN.issue({ + issuer: bob, + audience: service, + capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], + proofs: [cid1, cid2], + }) + + const res = await mf.dispatchFetch('http://localhost:8787/raw', { + method: 'POST', + headers: { + Authorization: `Bearer ${UCAN.format(ucan)}`, + }, + }) + const rsp = await res.json() + t.deepEqual(rsp, { + error: { + code: 'HTTP_ERROR', + message: 'Missing Proofs', + cause: { + prf: [cid1.toString(), cid2.toString()], + }, + }, + }) + }) + + test('should multiple invocation should pass', async function () { + const { mf, service } = ctx + + const alice = await Signer.generate() + const bob = await Signer.generate() + const proof1 = await UCAN.issue({ + issuer: alice, + audience: bob, + capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], + }) + + const cid1 = await UCAN.link(proof1) + const ucan1 = await UCAN.issue({ + issuer: bob, + audience: service, + capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], + proofs: [cid1], + }) + + const ucan2 = await UCAN.issue({ + issuer: bob, + audience: service, + capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], + proofs: [cid1], + }) + + const headers = new Headers() + headers.append('Authorization', `Bearer ${UCAN.format(ucan1)}`) + headers.append('Authorization', `Bearer ${UCAN.format(ucan2)}`) + headers.append('ucan', `${cid1.toString()} ${UCAN.format(proof1)}`) + + const res = await mf.dispatchFetch('http://localhost:8787/raw', { + method: 'POST', + headers: Object.fromEntries(headers.entries()), + }) + + const rsp = await res.json() + t.deepEqual(rsp, ['test pass', 'test pass']) }) - - const rsp = await res.json() - t.deepEqual(rsp, ['test pass', 'test pass']) }) diff --git a/packages/access-api/test/voucher-claim.test.js b/packages/access-api/test/voucher-claim.test.js index 1458924fa..54687cece 100644 --- a/packages/access-api/test/voucher-claim.test.js +++ b/packages/access-api/test/voucher-claim.test.js @@ -1,57 +1,69 @@ import { Delegation } from '@ucanto/core' import * as Voucher from '@web3-storage/capabilities/voucher' import { stringToDelegation } from '@web3-storage/access/encoding' -import { context, test } from './helpers/context.js' +import { context } from './helpers/context.js' +import assert from 'assert' -test.beforeEach(async (t) => { - t.context = await context() -}) +/** @type {typeof assert} */ +const t = assert +const test = it + +describe('ucan', function () { + /** @type {Awaited>} */ + let ctx + beforeEach(async function () { + ctx = await context() + }) -test('should voucher/claim', async (t) => { - const { issuer, service, conn } = t.context - - const inv = await Voucher.claim - .invoke({ - issuer, - audience: service, - with: issuer.did(), - nb: { - identity: 'mailto:email@dag.house', - product: 'product:free', - service: service.did(), - }, - }) - .execute(conn) - - if (!inv) { - return t.fail('no output') - } - if (inv.error) { - return t.fail(inv.message) - } - - const delegation = await stringToDelegation(inv) - - t.deepEqual(delegation.issuer.did(), service.did()) - t.deepEqual(delegation.audience.did(), issuer.did()) - t.deepEqual(delegation.capabilities[0].nb.space, issuer.did()) - t.deepEqual(delegation.capabilities[0].nb.product, 'product:free') - t.deepEqual(delegation.capabilities[0].nb.identity, 'mailto:email@dag.house') - - if (Delegation.isDelegation(delegation.proofs[0])) { - t.deepEqual(delegation.proofs[0].issuer.did(), service.did()) - t.deepEqual(delegation.proofs[0].capabilities, [ - { - with: service.did(), - can: 'voucher/redeem', + test('should voucher/claim', async function () { + const { issuer, service, conn } = ctx + + const inv = await Voucher.claim + .invoke({ + issuer, + audience: service, + with: issuer.did(), nb: { - space: 'did:*', - identity: 'mailto:*', - product: 'product:*', + identity: 'mailto:email@dag.house', + product: 'product:free', + service: service.did(), + }, + }) + .execute(conn) + + if (!inv) { + return t.fail('no output') + } + if (inv.error) { + return t.fail(inv.message) + } + + const delegation = await stringToDelegation(inv) + + t.deepEqual(delegation.issuer.did(), service.did()) + t.deepEqual(delegation.audience.did(), issuer.did()) + t.deepEqual(delegation.capabilities[0].nb.space, issuer.did()) + t.deepEqual(delegation.capabilities[0].nb.product, 'product:free') + t.deepEqual( + delegation.capabilities[0].nb.identity, + 'mailto:email@dag.house' + ) + + if (Delegation.isDelegation(delegation.proofs[0])) { + t.deepEqual(delegation.proofs[0].issuer.did(), service.did()) + t.deepEqual(delegation.proofs[0].capabilities, [ + { + with: service.did(), + can: 'voucher/redeem', + nb: { + space: 'did:*', + identity: 'mailto:*', + product: 'product:*', + }, }, - }, - ]) - } else { - t.fail('proof should be a delegation') - } + ]) + } else { + t.fail('proof should be a delegation') + } + }) }) diff --git a/packages/access-api/test/voucher-redeem.test.js b/packages/access-api/test/voucher-redeem.test.js index 381accbd4..586b73f6a 100644 --- a/packages/access-api/test/voucher-redeem.test.js +++ b/packages/access-api/test/voucher-redeem.test.js @@ -1,51 +1,182 @@ /* eslint-disable unicorn/prefer-number-properties */ import * as Voucher from '@web3-storage/capabilities/voucher' +import * as Top from '@web3-storage/capabilities/top' import { stringToDelegation } from '@web3-storage/access/encoding' -import { context, test } from './helpers/context.js' -import { createSpace } from './helpers/utils.js' +import { context } from './helpers/context.js' import { Spaces } from '../src/kvs/spaces.js' import { Signer } from '@ucanto/principal/ed25519' +// @ts-ignore +import isSubset from 'is-subset' -test.beforeEach(async (t) => { - t.context = await context() -}) +import assert from 'assert' -test('should return voucher/redeem', async (t) => { - const { issuer, service, conn, mf, db } = t.context +/** @type {typeof assert} */ +const t = assert +const test = it - const space = await Signer.generate() - const claim = await Voucher.claim - .invoke({ - issuer, - audience: service, - with: space.did(), - nb: { - identity: 'mailto:email@dag.house', +describe('ucan', function () { + /** @type {Awaited>} */ + let ctx + beforeEach(async function () { + ctx = await context() + }) + + test('should return voucher/redeem', async function () { + const { issuer, service, conn, db } = ctx + + const space = await Signer.generate() + const claim = await Voucher.claim + .invoke({ + issuer, + audience: service, + with: space.did(), + nb: { + identity: 'mailto:email@dag.house', + product: 'product:free', + service: service.did(), + }, + proofs: [ + await Top.top.delegate({ + issuer: space, + audience: issuer, + with: space.did(), + expiration: Infinity, + }), + ], + }) + .execute(conn) + + if (!claim) { + return t.fail('no output') + } + if (claim.error) { + return t.fail(claim.message) + } + + const delegation = await stringToDelegation(claim) + + const redeem = await Voucher.redeem + .invoke({ + issuer, + audience: service, + with: service.did(), + nb: { + space: space.did(), + identity: delegation.capabilities[0].nb.identity, + product: delegation.capabilities[0].nb.product, + }, + proofs: [ + delegation, + await Top.top.delegate({ + issuer: space, + audience: service, + with: space.did(), + expiration: Infinity, + }), + ], + facts: [{ space: { name: 'test' } }], + }) + + .execute(conn) + + if (redeem?.error) { + return t.fail() + } + + const spaces = new Spaces(db) + + // check db for space + t.ok( + isSubset(await spaces.get(space.did()), { + did: space.did(), product: 'product:free', - service: service.did(), - }, - proofs: [ - await Voucher.top.delegate({ - issuer: space, - audience: issuer, - with: space.did(), - expiration: Infinity, - }), - ], - }) - .execute(conn) + email: 'email@dag.house', + agent: issuer.did(), + }) + ) + + // check space delegations + const results = await spaces.getByEmail('email@dag.house') - if (!claim) { - return t.fail('no output') - } - if (claim.error) { - return t.fail(claim.message) - } + if (!results) { + return t.fail('no delegation for email') + } - const delegation = await stringToDelegation(claim) + if (!results[0].delegation) { + return t.fail('no delegation for email') + } + + const del = results[0].delegation + + t.deepEqual(del.audience.did(), service.did()) + t.deepEqual(del.capabilities[0].can, '*') + t.deepEqual(del.capabilities[0].with, space.did()) + // eslint-disable-next-line unicorn/no-null + t.deepEqual(del.facts[0], null) + }) - const redeem = await Voucher.redeem - .invoke({ + test('should fail with wrong resource', async function () { + const { issuer, service, conn } = ctx + + const redeem = await Voucher.redeem + .invoke({ + issuer, + audience: service, + with: issuer.did(), + nb: { + space: issuer.did(), + identity: 'mailto:email@dag.house', + product: 'product:free', + }, + }) + .execute(conn) + + if (redeem.error) { + t.ok(redeem.error) + t.deepEqual( + redeem.message, + `Resource ${issuer.did()} does not match service did ${service.did()}` + ) + } else { + t.fail('should fail') + } + }) + + test('should fail multiple voucher/redeem with same space did', async function () { + const { issuer, service, conn } = ctx + + const space = await Signer.generate() + const claim = await Voucher.claim + .invoke({ + issuer, + audience: service, + with: space.did(), + nb: { + identity: 'mailto:email@dag.house', + product: 'product:free', + service: service.did(), + }, + proofs: [ + await Top.top.delegate({ + issuer: space, + audience: issuer, + with: space.did(), + expiration: Infinity, + }), + ], + }) + .execute(conn) + + if (!claim) { + return t.fail('no output') + } + if (claim.error) { + return t.fail(claim.message) + } + + const delegation = await stringToDelegation(claim) + + const redeemInv = Voucher.redeem.invoke({ issuer, audience: service, with: service.did(), @@ -63,93 +194,20 @@ test('should return voucher/redeem', async (t) => { expiration: Infinity, }), ], + facts: [{ space: { name: 'test' } }], }) - .execute(conn) + const redeem = await redeemInv.execute(conn) - if (redeem?.error) { - return t.fail() - } + if (redeem?.error) { + return t.fail() + } - // @ts-ignore - const spaces = new Spaces(await mf.getKVNamespace('SPACES'), db) + const redeem2 = await redeemInv.execute(conn) - // check db for space - t.like(await spaces.get(space.did()), { - did: space.did(), - product: 'product:free', - email: 'email@dag.house', - agent: issuer.did(), + t.ok(redeem2.error) + if (redeem2.error) { + t.deepEqual(redeem2.message, `Space ${space.did()} already registered.`) + } }) - - // check space delegations - const delegations = await spaces.getDelegations('mailto:email@dag.house') - - if (!delegations) { - return t.fail('no delegation for email') - } - - const del = await stringToDelegation(delegations[0]) - - t.deepEqual(del.audience.did(), service.did()) - t.deepEqual(del.capabilities[0].can, '*') - t.deepEqual(del.capabilities[0].with, space.did()) -}) - -test('should save first space delegation', async (t) => { - const { issuer, service, conn, mf } = t.context - - await createSpace(issuer, service, conn, 'first@dag.house') - - const spaces = await mf.getKVNamespace('SPACES') - - const delEncoded = await spaces.get('mailto:first@dag.house', { - type: 'json', - }) - - // @ts-ignore - t.assert(delEncoded.length === 1) -}) - -test('should save multiple space delegation', async (t) => { - const { issuer, service, conn, mf } = t.context - - await createSpace(issuer, service, conn, 'multiple@dag.house') - await createSpace(issuer, service, conn, 'multiple@dag.house') - - const spaces = await mf.getKVNamespace('SPACES') - - const delEncoded = await spaces.get('mailto:multiple@dag.house', { - type: 'json', - }) - - // @ts-ignore - t.assert(delEncoded.length === 2) -}) - -test('should fail with wrong resource', async (t) => { - const { issuer, service, conn } = t.context - - const redeem = await Voucher.redeem - .invoke({ - issuer, - audience: service, - with: issuer.did(), - nb: { - space: issuer.did(), - identity: 'mailto:email@dag.house', - product: 'product:free', - }, - }) - .execute(conn) - - if (redeem.error) { - t.true(redeem.error) - t.deepEqual( - redeem.message, - `Resource ${issuer.did()} does not service did ${service.did()}` - ) - } else { - t.fail('should fail') - } }) diff --git a/packages/access-api/tsconfig.json b/packages/access-api/tsconfig.json index 8f439d4e1..da475de58 100644 --- a/packages/access-api/tsconfig.json +++ b/packages/access-api/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist", - "types": ["@cloudflare/workers-types"], + "types": ["@cloudflare/workers-types", "mocha"], "jsx": "react-jsx", "jsxImportSource": "preact" }, diff --git a/packages/access-client/package.json b/packages/access-client/package.json index 63e8ac9b4..9d9910c5f 100644 --- a/packages/access-client/package.json +++ b/packages/access-client/package.json @@ -80,8 +80,8 @@ "devDependencies": { "@types/assert": "^1.5.6", "@types/inquirer": "^9.0.3", - "@types/mocha": "^10.0.0", - "@types/node": "^18.11.10", + "@types/mocha": "^10.0.1", + "@types/node": "^18.11.9", "@types/ws": "^8.5.3", "@ucanto/server": "^3.0.4", "assert": "^2.0.0", diff --git a/packages/access-client/src/agent.js b/packages/access-client/src/agent.js index ec967a0d5..4fad41faa 100644 --- a/packages/access-client/src/agent.js +++ b/packages/access-client/src/agent.js @@ -14,6 +14,7 @@ import * as Voucher from '@web3-storage/capabilities/voucher' import { stringToDelegation } from './encoding.js' import { Websocket, AbortError } from './utils/ws.js' import { Signer } from '@ucanto/principal/ed25519' +import { Verifier } from '@ucanto/principal' import { invoke, delegate } from '@ucanto/core' import { isExpired, @@ -76,6 +77,9 @@ export class Agent { /** @type {typeof fetch} */ #fetch + /** @type {import('./types').AgentData} */ + #data + /** * @param {import('./types').AgentOptions} opts */ @@ -83,10 +87,11 @@ export class Agent { this.url = opts.url || new URL(HOST) this.connection = opts.connection this.issuer = opts.data.principal + this.meta = opts.data.meta this.store = opts.store - this.data = opts.data // private + this.#data = opts.data this.#fetch = opts.fetch this.#service = undefined } @@ -123,6 +128,10 @@ export class Agent { }) } + get spaces() { + return this.#data.spaces + } + async service() { if (this.#service) { return this.#service @@ -134,7 +143,7 @@ export class Agent { } did() { - return this.data.principal.did() + return this.#data.principal.did() } /** @@ -150,11 +159,12 @@ export class Agent { checkIsExpired: true, }) - this.data.delegations.set(delegation.cid.toString(), { + this.#data.delegations.set(delegation.cid.toString(), { delegation, + meta: { audience: this.meta }, }) - await this.store.save(this.data) + await this.store.save(this.#data) } /** @@ -164,7 +174,7 @@ export class Agent { */ async *#delegations(caps) { const _caps = new Set(caps) - for (const [key, value] of this.data.delegations) { + for (const [key, value] of this.#data.delegations) { // check expiration if (!isExpired(value.delegation)) { // check if delegation can be used @@ -183,11 +193,11 @@ export class Agent { } } else { // delete any expired delegation - this.data.delegations.delete(key) + this.#data.delegations.delete(key) } } - await this.store.save(this.data) + await this.store.save(this.#data) } /** @@ -199,6 +209,7 @@ export class Agent { */ async proofs(caps) { const arr = [] + for await (const value of this.#delegations(caps)) { if (value.delegation.audience.did() === this.issuer.did()) { arr.push(value.delegation) @@ -246,19 +257,95 @@ export class Agent { expiration: Infinity, }) - this.data.spaces.set(signer.did(), { + const meta = { name, isRegistered: false, - }) + } + this.#data.spaces.set(signer.did(), meta) await this.addProof(proof) return { did: signer.did(), + meta, proof, } } + /** + * Import a space from a '*' delegation + * + * @param {Ucanto.Delegation} delegation + */ + async importSpaceFromDelegation(delegation) { + if (delegation.capabilities[0].can !== '*') { + throw new Error( + 'Space can only be import with full capabilities delegation.' + ) + } + + const meta = /** @type {import('./types').SpaceMeta} */ ( + delegation.facts[0].space + ) + const del = /** @type {Ucanto.Delegation<[import('./types').Top]>} */ ( + delegation + ) + // @ts-ignore + const did = Verifier.parse(del.capabilities[0].with).did() + + this.#data.spaces.set(did, meta) + + await this.addProof(del) + + return { + did, + meta, + proof: del, + } + } + + /** + * + * @param {string} email + * @param {object} [opts] + * @param {AbortSignal} [opts.signal] + */ + async recover(email, opts) { + const service = await this.service() + const inv = await this.invokeAndExecute(Space.recoverValidation, { + with: URI.from(this.did()), + nb: { identity: URI.from(`mailto:${email}`) }, + }) + + if (inv && inv.error) { + throw new Error('Recover validation failed', { cause: inv }) + } + + const spaceRecover = + /** @type {Ucanto.Delegation<[import('./types').SpaceRecover]>} */ ( + await this.#waitForDelegation(opts) + ) + await this.addProof(spaceRecover) + + const recoverInv = await this.invokeAndExecute(Space.recover, { + with: URI.from(service.did()), + nb: { + identity: URI.from(`mailto:${email}`), + }, + }) + + if (recoverInv && recoverInv.error) { + throw new Error('Spaces recover failed', { cause: recoverInv }) + } + + const dels = [] + for (const del of recoverInv) { + dels.push(await stringToDelegation(del)) + } + + return dels + } + /** * Sets the current selected space * @@ -278,8 +365,8 @@ export class Agent { throw new Error(`Agent has no proofs for ${space}.`) } - this.data.currentSpace = space - await this.store.save(this.data) + this.#data.currentSpace = space + await this.store.save(this.#data) return space } @@ -288,14 +375,14 @@ export class Agent { * Get current space DID */ currentSpace() { - return this.data.currentSpace + return this.#data.currentSpace } /** * Get current space DID, proofs and abilities */ async currentSpaceWithMeta() { - if (!this.data.currentSpace) { + if (!this.#data.currentSpace) { return } @@ -303,7 +390,7 @@ export class Agent { const proofs = await this.proofs([ { can: 'space/info', - with: this.data.currentSpace, + with: this.#data.currentSpace, }, ]) @@ -315,9 +402,10 @@ export class Agent { } return { - did: this.data.currentSpace, + did: this.#data.currentSpace, proofs, capabilities: [...caps], + meta: this.#data.spaces.get(this.#data.currentSpace), } } @@ -333,7 +421,7 @@ export class Agent { async registerSpace(email, opts) { const space = this.currentSpace() const service = await this.service() - const spaceMeta = space ? this.data.spaces.get(space) : undefined + const spaceMeta = space ? this.#data.spaces.get(space) : undefined if (!space || !spaceMeta) { throw new Error('No space selected') @@ -355,7 +443,10 @@ export class Agent { throw new Error('Voucher claim failed', { cause: inv }) } - const voucherRedeem = await this.#waitForVoucherRedeem(opts) + const voucherRedeem = + /** @type {Ucanto.Delegation<[import('./types').VoucherRedeem]>} */ ( + await this.#waitForDelegation(opts) + ) await this.addProof(voucherRedeem) const delegationToService = await this.delegate({ abilities: ['*'], @@ -375,6 +466,12 @@ export class Agent { product: voucherRedeem.capabilities[0].nb.product, }, proofs: [delegationToService], + facts: [ + { + space: spaceMeta, + agent: this.meta, + }, + ], }) if (accInv && accInv.error) { @@ -383,10 +480,10 @@ export class Agent { spaceMeta.isRegistered = true - this.data.spaces.set(space, spaceMeta) - this.data.delegations.delete(voucherRedeem.cid.toString()) + this.#data.spaces.set(space, spaceMeta) + this.#data.delegations.delete(voucherRedeem.cid.toString()) - this.store.save(this.data) + this.store.save(this.#data) } /** @@ -394,7 +491,7 @@ export class Agent { * @param {object} [opts] * @param {AbortSignal} [opts.signal] */ - async #waitForVoucherRedeem(opts) { + async #waitForDelegation(opts) { const ws = new Websocket(this.url, 'validate-ws') await ws.open() @@ -411,21 +508,17 @@ export class Agent { } if (msg.type === 'delegation') { - const delegation = await stringToDelegation( - /** @type {import('./types').EncodedDelegation<[import('./types').VoucherRedeem]>} */ ( - msg.delegation - ) - ) + const delegation = await stringToDelegation(msg.delegation) ws.close() return delegation } } catch (error) { if (error instanceof AbortError) { await ws.close() - throw new TypeError('Failed to get voucher/redeem', { cause: error }) + throw new TypeError('Failed to get delegation', { cause: error }) } } - throw new TypeError('Failed to get voucher/redeem') + throw new TypeError('Failed to get delegation') } /** @@ -451,16 +544,17 @@ export class Agent { issuer: this.issuer, capabilities: caps, proofs: await this.proofs(caps), + facts: [{ space: space.meta }], ...options, }) - this.data.delegations.set(delegation.cid.toString(), { + this.#data.delegations.set(delegation.cid.toString(), { delegation, meta: { audience: options.audienceMeta, }, }) - await this.store.save(this.data) + await this.store.save(this.#data) return delegation } @@ -562,7 +656,7 @@ export class Agent { }, ]) - if (proofs.length === 0) { + if (proofs.length === 0 && options.with !== this.did()) { throw new Error( `no proofs available for resource ${space} and ability ${cap.can}` ) @@ -570,6 +664,7 @@ export class Agent { const extraProofs = options.proofs || [] const inv = invoke({ + ...options, audience: options.audience || (await this.service()), // @ts-ignore capability: cap.create({ diff --git a/packages/access-client/src/cli/cmd-create-account.js b/packages/access-client/src/cli/cmd-create-space.js similarity index 59% rename from packages/access-client/src/cli/cmd-create-account.js rename to packages/access-client/src/cli/cmd-create-space.js index 763c11a18..e5ee8a8f9 100644 --- a/packages/access-client/src/cli/cmd-create-account.js +++ b/packages/access-client/src/cli/cmd-create-space.js @@ -9,7 +9,7 @@ import { getService } from './utils.js' /** * @param {{ profile: any; env: string }} opts */ -export async function cmdCreateAccount(opts) { +export async function cmdCreateSpace(opts) { const { url } = await getService(opts.env) const store = new StoreConf({ profile: opts.profile }) @@ -21,16 +21,26 @@ export async function cmdCreateAccount(opts) { }) 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...') + const { email, name } = await inquirer.prompt([ + { + type: 'input', + name: 'name', + message: 'Input your a name for the new space:', + }, + { + type: 'input', + name: 'email', + default: 'hugomrdias@gmail.com', + message: 'Input your email to validate:', + }, + ]) try { + spinner.start('Waiting for email validation...') + const space = await agent.createSpace(name) + + await agent.setCurrentSpace(space.did) await agent.registerSpace(email) - spinner.succeed('Account has been created and register with the service.') + spinner.succeed('Space has been created and register with the service.') } catch (error) { console.error(error) // @ts-ignore diff --git a/packages/access-client/src/cli/cmd-whoami.js b/packages/access-client/src/cli/cmd-whoami.js index f3c5c3a14..cde9105c7 100644 --- a/packages/access-client/src/cli/cmd-whoami.js +++ b/packages/access-client/src/cli/cmd-whoami.js @@ -1,31 +1,48 @@ /* eslint-disable no-console */ -// import { StoreConf } from '../stores/store-conf.js' -// import { NAME } from './config.js' +import { Agent } from '../agent.js' +import { expirationToDate } from '../encoding.js' +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`) - // } + const store = new StoreConf({ profile: opts.profile }) + if (await store.exists()) { + const agent = await Agent.create({ + store, + }) + console.log('Agent', agent.issuer.did(), agent.meta) + console.log('Current Space', await agent.currentSpaceWithMeta()) + console.log('\nSpaces:') + for (const space of agent.spaces) { + console.log( + `Name: ${space[1].name} DID: ${space[0]} Registered: ${space[1].isRegistered}` + ) + } + console.log('\nProofs:') + for (const proof of await agent.proofs()) { + for (const cap of proof.capabilities) { + console.log( + `With resource: ${cap.with} can "${cap.can}" expires at ${proof.expiration}` + ) + } + } + + console.log('\nDelegations:') + for await (const { meta, delegation } of agent.delegationsWithMeta()) { + console.log(`Audience ${meta.audience.name} (${meta.audience.type}):`) + for (const cap of delegation.capabilities) { + const expires = expirationToDate(delegation.expiration) + console.log( + `With resource: ${cap.with} can "${cap.can}" expires at ${ + expires ? expires.toISOString() : 'never' + }` + ) + } + } + } else { + console.error(`Run "${NAME} setup" first`) + } } diff --git a/packages/access-client/src/cli/index.js b/packages/access-client/src/cli/index.js index 878c8db56..1008da487 100755 --- a/packages/access-client/src/cli/index.js +++ b/packages/access-client/src/cli/index.js @@ -3,19 +3,20 @@ import fs from 'fs' import sade from 'sade' import { NAME, pkg } from './config.js' -import { getService } from './utils.js' +import { getService, selectSpace } from './utils.js' // @ts-ignore // eslint-disable-next-line no-unused-vars -import { cmdCreateAccount } from './cmd-create-account.js' +import { cmdCreateSpace } from './cmd-create-space.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 { stringToDelegation } from '../encoding.js' -// import inquirer from 'inquirer' -// import { Verifier } from '@ucanto/principal/ed25519' -// import { delegationToString, stringToDelegation } from '../encoding.js' +import { abilitiesAsStrings } from '@web3-storage/capabilities' +import { delegationToString, stringToDelegation } from '../encoding.js' +import inquirer from 'inquirer' +import { Verifier } from '@ucanto/principal' +import path from 'path' const prog = sade(NAME) prog @@ -31,111 +32,104 @@ prog .action(cmdSetup) prog.command('whoami').describe('Print config file content.').action(cmdWhoami) prog - .command('create-account') - .describe('Create new account.') - .action(cmdCreateAccount) + .command('create-space') + .describe('Create new space and register with the service.') + .action(cmdCreateSpace) prog - .command('account') - .describe('Account info.') + .command('space') + .describe('Space info.') .action(async (opts) => { - // const store = new StoreConf({ profile: opts.profile }) - // const { url } = await getService(opts.env) - // if (await store.exists()) { - // 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: 'account', - // default: 'device', - // choices, - // message: 'Select account:', - // }, - // ]) - // try { - // const result = await agent.getAccountInfo(account) - // console.log(result) - // } catch (error_) { - // const error = /** @type {Error} */ (error_) - // console.log(error.message) - // } - // } else { - // console.error(`Run "${NAME} setup" first`) - // } + const store = new StoreConf({ profile: opts.profile }) + const { url } = await getService(opts.env) + if (await store.exists()) { + const agent = await Agent.create({ + store, + url, + }) + const space = await selectSpace(agent) + try { + const result = await agent.getSpaceInfo(space) + console.log(result) + } catch (error_) { + const error = /** @type {Error} */ (error_) + console.log(error.message) + } + } else { + console.error(`Run "${NAME} setup" first`) + } }) prog .command('delegate') .describe('Delegation capabilities.') + .option('--file', 'File to write the delegation into.') .action(async (opts) => { - // const store = new StoreConf({ profile: opts.profile }) - // const { url } = await getService(opts.env) - // if (await store.exists()) { - // const agent = await Agent.create({ - // store, - // url, - // }) - // 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:', - // }, - // ]) - // 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`) - // } + const store = new StoreConf({ profile: opts.profile }) + const { url } = await getService(opts.env) + if (await store.exists()) { + const agent = await Agent.create({ + store, + url, + }) + const space = await selectSpace(agent) + + await agent.setCurrentSpace(space) + + const { audience, expiration, name, type, abilities } = + await inquirer.prompt([ + { + type: 'input', + name: 'audience', + message: 'Input audience DID:', + }, + { + type: 'input', + name: 'name', + message: 'Input audience name:', + }, + { + type: 'list', + name: 'type', + default: 'device', + choices: ['device', 'app', 'service'], + message: 'Input audience type:', + }, + { + type: 'number', + name: 'expiration', + message: 'Input expiration in seconds:', + }, + { + type: 'checkbox', + name: 'abilities', + message: 'Input abilities to delegate:', + choices: abilitiesAsStrings, + }, + ]) + + const delegation = await agent.delegate({ + audience: Verifier.parse(audience), + audienceMeta: { + name, + type, + }, + lifetimeInSeconds: isNaN(expiration) ? Infinity : expiration, + abilities, + }) + + const delString = await delegationToString(delegation) + + if (opts.file) { + fs.writeFileSync(path.join(process.cwd(), opts.file), delString, { + encoding: 'utf8', + }) + } else { + console.log(delString) + } + } else { + console.error(`Run "${NAME} setup" first`) + } }) prog @@ -151,9 +145,43 @@ prog url, }) - const del = fs.readFileSync('./delegation', { encoding: 'utf8' }) + const del = fs.readFileSync(path.resolve(opts.delegation), { + encoding: 'utf8', + }) + + await agent.importSpaceFromDelegation(await stringToDelegation(del)) + } else { + console.error(`Run "${NAME} setup" first`) + } + }) + +prog + .command('recover') + .describe('Recover spaces with email.') + .action(async (opts) => { + const store = new StoreConf({ profile: opts.profile }) + const { url } = await getService(opts.env) + if (await store.exists()) { + const agent = await Agent.create({ + store, + url, + }) + + const { email } = await inquirer.prompt([ + { + type: 'input', + name: 'email', + default: 'hugomrdias@gmail.com', + message: 'Input email:', + }, + ]) + + const dels = await agent.recover(email) - await agent.addProof(await stringToDelegation(del)) + for (const del of dels) { + const { did, meta } = await agent.importSpaceFromDelegation(del) + console.log(`Imported space ${meta.name} with DID: ${did}`) + } } else { console.error(`Run "${NAME} setup" first`) } diff --git a/packages/access-client/src/cli/utils.js b/packages/access-client/src/cli/utils.js index 8f67174d1..24acb6c71 100644 --- a/packages/access-client/src/cli/utils.js +++ b/packages/access-client/src/cli/utils.js @@ -1,4 +1,8 @@ +// @ts-ignore +// eslint-disable-next-line no-unused-vars +import * as Ucanto from '@ucanto/interface' import { Verifier } from '@ucanto/principal/ed25519' +import inquirer from 'inquirer' /** @type {Record} */ const envs = { @@ -29,3 +33,24 @@ export async function getService(env) { return { url, audience } } } + +/** + * @template {Ucanto.Signer} T + * @param {import('../agent').Agent} agent + */ +export async function selectSpace(agent) { + const choices = [] + for (const [key, value] of agent.spaces) { + choices.push({ name: value.name, value: key }) + } + const { space } = await inquirer.prompt([ + { + type: 'list', + name: 'space', + choices, + message: 'Select space:', + }, + ]) + + return space +} diff --git a/packages/access-client/src/encoding.js b/packages/access-client/src/encoding.js index 535360aa5..eeb073ad0 100644 --- a/packages/access-client/src/encoding.js +++ b/packages/access-client/src/encoding.js @@ -122,3 +122,15 @@ export async function stringToDelegation(raw, encoding) { return /** @type {Types.Delegation} */ (delegations[0]) } + +/** + * @param {number} [expiration] + */ +export function expirationToDate(expiration) { + const expires = + expiration === Infinity || !expiration + ? undefined + : new Date(expiration * 1000) + + return expires +} diff --git a/packages/access-client/src/stores/store-conf.js b/packages/access-client/src/stores/store-conf.js index 97732e7c3..d8a59464d 100644 --- a/packages/access-client/src/stores/store-conf.js +++ b/packages/access-client/src/stores/store-conf.js @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/no-null */ /* eslint-disable jsdoc/check-indentation */ import Conf from 'conf' import { Signer } from '@ucanto/principal/ed25519' @@ -14,7 +15,7 @@ import * as Ucanto from '@ucanto/interface' * currentSpace?: Ucanto.DID * spaces: Array<[Ucanto.DID, import('../types').SpaceMeta]> * delegations: Array<[import('../types').CIDString, { - * meta?: import('../types').DelegationMeta, + * meta: import('../types').DelegationMeta, * delegation: import('../types.js').EncodedDelegation * }]> * }} Data @@ -106,7 +107,7 @@ export class StoreConf { } /** @type {Data} */ const encodedData = { - currentSpace: data.currentSpace, + currentSpace: data.currentSpace || undefined, spaces: [...data.spaces.entries()], meta: data.meta, principal: Signer.format(data.principal), @@ -134,7 +135,7 @@ export class StoreConf { /** @type {StoreData} */ return { principal: Signer.parse(data.principal), - currentSpace: data.currentSpace, + currentSpace: data.currentSpace === null ? undefined : data.currentSpace, meta: data.meta, spaces: new Map(data.spaces), delegations: dels, diff --git a/packages/access-client/src/stores/types.ts b/packages/access-client/src/stores/types.ts index ed01d7adc..dbc98747f 100644 --- a/packages/access-client/src/stores/types.ts +++ b/packages/access-client/src/stores/types.ts @@ -56,7 +56,7 @@ export interface StoreDataIDB { delegations: Map< CIDString, { - meta?: DelegationMeta + meta: DelegationMeta delegation: Array<{ cid: CIDString; bytes: Uint8Array }> } > diff --git a/packages/access-client/src/types.ts b/packages/access-client/src/types.ts index f3477d8ec..c295a62d9 100644 --- a/packages/access-client/src/types.ts +++ b/packages/access-client/src/types.ts @@ -82,7 +82,7 @@ export interface AgentData { principal: T currentSpace?: DID spaces: Map - delegations: Map + delegations: Map } /** diff --git a/packages/access-client/test/stores/store-indexeddb.browser.test.js b/packages/access-client/test/stores/store-indexeddb.browser.test.js index b1f15df78..aef7f67ad 100644 --- a/packages/access-client/test/stores/store-indexeddb.browser.test.js +++ b/packages/access-client/test/stores/store-indexeddb.browser.test.js @@ -70,7 +70,10 @@ describe('IndexedDB store', () => { expiration: Infinity, }) - data0.delegations.set(del0.cid.toString(), { delegation: del0 }) + data0.delegations.set(del0.cid.toString(), { + delegation: del0, + meta: { audience: { name: 'test', type: 'device' } }, + }) await store.save(data0) const data1 = await store.load() diff --git a/packages/capabilities/package.json b/packages/capabilities/package.json index a669461e3..4f74dcf87 100644 --- a/packages/capabilities/package.json +++ b/packages/capabilities/package.json @@ -71,6 +71,7 @@ "hd-scripts": "^3.0.2", "mocha": "^10.1.0", "playwright-test": "^8.1.1", + "type-fest": "^3.3.0", "typescript": "4.8.4", "watch": "^1.0.2" }, diff --git a/packages/capabilities/src/index.js b/packages/capabilities/src/index.js index e54f7cc10..b3bf5043a 100644 --- a/packages/capabilities/src/index.js +++ b/packages/capabilities/src/index.js @@ -1,6 +1,27 @@ -export * as Space from './space.js' -export * as Top from './top.js' -export * as Store from './store.js' -export * as Upload from './upload.js' -export * as Voucher from './voucher.js' -export * as Utils from './utils.js' +import * as Space from './space.js' +import * as Top from './top.js' +import * as Store from './store.js' +import * as Upload from './upload.js' +import * as Voucher from './voucher.js' +import * as Utils from './utils.js' + +export { Space, Top, Store, Upload, Voucher, Utils } + +/** @type {import('./types').AbilitiesArray} */ +export const abilitiesAsStrings = [ + Top.top.can, + Space.space.can, + Space.info.can, + Space.recover.can, + Space.recoverValidation.can, + Upload.upload.can, + Upload.add.can, + Upload.remove.can, + Upload.list.can, + Store.store.can, + Store.add.can, + Store.remove.can, + Store.list.can, + Voucher.claim.can, + Voucher.redeem.can, +] diff --git a/packages/capabilities/src/types.ts b/packages/capabilities/src/types.ts index 3db23a629..01b22ce83 100644 --- a/packages/capabilities/src/types.ts +++ b/packages/capabilities/src/types.ts @@ -1,3 +1,4 @@ +import type { TupleToUnion } from 'type-fest' import { InferInvokedCapability } from '@ucanto/interface' import { space, info, recover, recoverValidation } from './space.js' import { top } from './top.js' @@ -29,19 +30,22 @@ export type StoreList = InferInvokedCapability // Top export type Top = InferInvokedCapability -export type Abilities = - | Space['can'] - | SpaceInfo['can'] - | SpaceRecover['can'] - | SpaceRecoverValidation['can'] - | VoucherClaim['can'] - | VoucherRedeem['can'] - | Upload['can'] - | UploadAdd['can'] - | UploadRemove['can'] - | UploadList['can'] - | Store['can'] - | StoreAdd['can'] - | StoreRemove['can'] - | StoreList['can'] - | Top['can'] +export type Abilities = TupleToUnion + +export type AbilitiesArray = [ + Top['can'], + Space['can'], + SpaceInfo['can'], + SpaceRecover['can'], + SpaceRecoverValidation['can'], + Upload['can'], + UploadAdd['can'], + UploadRemove['can'], + UploadList['can'], + Store['can'], + StoreAdd['can'], + StoreRemove['can'], + StoreList['can'], + VoucherClaim['can'], + VoucherRedeem['can'] +] diff --git a/packages/upload-client/package.json b/packages/upload-client/package.json index 4b6a00c02..f40218d55 100644 --- a/packages/upload-client/package.json +++ b/packages/upload-client/package.json @@ -76,7 +76,7 @@ }, "devDependencies": { "@types/assert": "^1.5.6", - "@types/mocha": "^10.0.0", + "@types/mocha": "^10.0.1", "@ucanto/principal": "^3.0.0", "@ucanto/server": "^3.0.1", "assert": "^2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37deab271..4396fc7ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,7 +17,7 @@ importers: typedoc: 0.23.21_typescript@4.9.3 typedoc-plugin-missing-exports: 1.0.0_typedoc@0.23.21 devDependencies: - lint-staged: 13.1.0 + lint-staged: 13.0.4 prettier: 2.8.0 simple-git-hooks: 2.8.1 typescript: 4.9.3 @@ -32,7 +32,8 @@ importers: '@sentry/cli': 2.7.0 '@types/assert': ^1.5.6 '@types/git-rev-sync': ^2.0.0 - '@types/node': ^18.11.10 + '@types/mocha': ^10.0.1 + '@types/node': ^18.11.9 '@types/qrcode': ^1.5.0 '@ucanto/core': ^3.0.2 '@ucanto/interface': ^3.0.1 @@ -42,14 +43,15 @@ importers: '@web3-storage/access': workspace:^ '@web3-storage/capabilities': workspace:^ '@web3-storage/worker-utils': 0.4.3-dev - ava: ^5.1.0 - better-sqlite3: 8.0.1 + better-sqlite3: 8.0.0 buffer: ^6.0.3 dotenv: ^16.0.3 esbuild: ^0.15.16 git-rev-sync: ^3.0.2 hd-scripts: ^3.0.2 + is-subset: ^0.1.1 miniflare: ^2.11.0 + mocha: ^10.1.0 p-retry: ^5.1.2 p-wait-for: ^5.0.0 preact: ^10.11.3 @@ -85,16 +87,18 @@ importers: '@sentry/cli': 2.7.0 '@types/assert': 1.5.6 '@types/git-rev-sync': 2.0.0 + '@types/mocha': 10.0.1 '@types/node': 18.11.10 '@types/qrcode': 1.5.0 - ava: 5.1.0 - better-sqlite3: 8.0.1 + better-sqlite3: 8.0.0 buffer: 6.0.3 dotenv: 16.0.3 - esbuild: 0.15.18 + esbuild: 0.15.16 git-rev-sync: 3.0.2 hd-scripts: 3.0.2 + is-subset: 0.1.1 miniflare: 2.11.0 + mocha: 10.1.0 p-wait-for: 5.0.0 process: 0.11.10 readable-stream: 4.2.0 @@ -108,8 +112,8 @@ importers: '@ipld/dag-ucan': ^2.0.1 '@types/assert': ^1.5.6 '@types/inquirer': ^9.0.3 - '@types/mocha': ^10.0.0 - '@types/node': ^18.11.10 + '@types/mocha': ^10.0.1 + '@types/node': ^18.11.9 '@types/ws': ^8.5.3 '@ucanto/client': ^3.0.2 '@ucanto/core': ^3.0.2 @@ -215,7 +219,7 @@ importers: ava: 5.1.0 buffer: 6.0.3 dotenv: 16.0.3 - esbuild: 0.15.18 + esbuild: 0.15.16 git-rev-sync: 3.0.2 hd-scripts: 3.0.2 miniflare: 2.11.0 @@ -240,6 +244,7 @@ importers: hd-scripts: ^3.0.2 mocha: ^10.1.0 playwright-test: ^8.1.1 + type-fest: ^3.3.0 typescript: 4.8.4 watch: ^1.0.2 dependencies: @@ -256,6 +261,7 @@ importers: hd-scripts: 3.0.2 mocha: 10.1.0 playwright-test: 8.1.1 + type-fest: 3.3.0 typescript: 4.8.4 watch: 1.0.2 @@ -265,7 +271,7 @@ importers: '@ipld/dag-ucan': ^2.0.1 '@ipld/unixfs': ^2.0.0 '@types/assert': ^1.5.6 - '@types/mocha': ^10.0.0 + '@types/mocha': ^10.0.1 '@ucanto/client': ^3.0.1 '@ucanto/interface': ^3.0.0 '@ucanto/principal': ^3.0.0 @@ -483,8 +489,8 @@ packages: rollup-plugin-node-polyfills: 0.2.1 dev: true - /@esbuild/android-arm/0.15.18: - resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==} + /@esbuild/android-arm/0.15.16: + resolution: {integrity: sha512-nyB6CH++2mSgx3GbnrJsZSxzne5K0HMyNIWafDHqYy7IwxFc4fd/CgHVZXr8Eh+Q3KbIAcAe3vGyqIPhGblvMQ==} engines: {node: '>=12'} cpu: [arm] os: [android] @@ -492,8 +498,8 @@ packages: dev: true optional: true - /@esbuild/linux-loong64/0.15.18: - resolution: {integrity: sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==} + /@esbuild/linux-loong64/0.15.16: + resolution: {integrity: sha512-SDLfP1uoB0HZ14CdVYgagllgrG7Mdxhkt4jDJOKl/MldKrkQ6vDJMZKl2+5XsEY/Lzz37fjgLQoJBGuAw/x8kQ==} engines: {node: '>=12'} cpu: [loong64] os: [linux] @@ -999,7 +1005,7 @@ packages: engines: {node: '>= 8'} dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.14.0 + fastq: 1.13.0 dev: true /@phenomnomnominal/tsquery/4.2.0_typescript@4.9.3: @@ -1133,7 +1139,7 @@ packages: resolution: {integrity: sha512-CzNkWqQftcmk2jaCWdBTf9Sm7xSw4rkI1zpU/Udw3HX5//adEZUIm9STtoRP1qgWj0CWQtJ9UTvqmO2NNjhMJw==} dependencies: '@types/through': 0.0.30 - rxjs: 7.6.0 + rxjs: 7.5.7 dev: true /@types/istanbul-lib-coverage/2.0.4: @@ -1207,7 +1213,7 @@ packages: '@types/yargs-parser': 21.0.0 dev: true - /@typescript-eslint/eslint-plugin/5.45.0_yjegg5cyoezm3fzsmuszzhetym: + /@typescript-eslint/eslint-plugin/5.45.0_czs5uoqkd3podpy6vgtsxfc7au: resolution: {integrity: sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -1218,12 +1224,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.45.0_s5ps7njkmjlaqajutnox5ntcla + '@typescript-eslint/parser': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a '@typescript-eslint/scope-manager': 5.45.0 - '@typescript-eslint/type-utils': 5.45.0_s5ps7njkmjlaqajutnox5ntcla - '@typescript-eslint/utils': 5.45.0_s5ps7njkmjlaqajutnox5ntcla + '@typescript-eslint/type-utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a + '@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a debug: 4.3.4 - eslint: 8.29.0 + eslint: 8.28.0 ignore: 5.2.1 natural-compare-lite: 1.4.0 regexpp: 3.2.0 @@ -1234,20 +1240,20 @@ packages: - supports-color dev: true - /@typescript-eslint/experimental-utils/5.45.0_s5ps7njkmjlaqajutnox5ntcla: + /@typescript-eslint/experimental-utils/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a: resolution: {integrity: sha512-DnRQg5+3uHHt/gaifTjwg9OKbg9/TWehfJzYHQIDJboPEbF897BKDE/qoqMhW7nf0jWRV1mwVXTaUvtB1/9Gwg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.45.0_s5ps7njkmjlaqajutnox5ntcla - eslint: 8.29.0 + '@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a + eslint: 8.28.0 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/parser/5.45.0_s5ps7njkmjlaqajutnox5ntcla: + /@typescript-eslint/parser/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a: resolution: {integrity: sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -1261,7 +1267,7 @@ packages: '@typescript-eslint/types': 5.45.0 '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3 debug: 4.3.4 - eslint: 8.29.0 + eslint: 8.28.0 typescript: 4.9.3 transitivePeerDependencies: - supports-color @@ -1275,7 +1281,7 @@ packages: '@typescript-eslint/visitor-keys': 5.45.0 dev: true - /@typescript-eslint/type-utils/5.45.0_s5ps7njkmjlaqajutnox5ntcla: + /@typescript-eslint/type-utils/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a: resolution: {integrity: sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -1286,9 +1292,9 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3 - '@typescript-eslint/utils': 5.45.0_s5ps7njkmjlaqajutnox5ntcla + '@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a debug: 4.3.4 - eslint: 8.29.0 + eslint: 8.28.0 tsutils: 3.21.0_typescript@4.9.3 typescript: 4.9.3 transitivePeerDependencies: @@ -1321,7 +1327,7 @@ packages: - supports-color dev: true - /@typescript-eslint/utils/5.45.0_s5ps7njkmjlaqajutnox5ntcla: + /@typescript-eslint/utils/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a: resolution: {integrity: sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -1332,9 +1338,9 @@ packages: '@typescript-eslint/scope-manager': 5.45.0 '@typescript-eslint/types': 5.45.0 '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3 - eslint: 8.29.0 + eslint: 8.28.0 eslint-scope: 5.1.1 - eslint-utils: 3.0.0_eslint@8.29.0 + eslint-utils: 3.0.0_eslint@8.28.0 semver: 7.3.8 transitivePeerDependencies: - supports-color @@ -1834,8 +1840,8 @@ packages: /base64-js/1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - /better-sqlite3/8.0.1: - resolution: {integrity: sha512-JhTZjpyapA1icCEjIZB4TSSgkGdFgpWZA2Wszg7Cf4JwJwKQmbvuNnJBeR+EYG/Z29OXvR4G//Rbg31BW/Z7Yg==} + /better-sqlite3/8.0.0: + resolution: {integrity: sha512-DhIPmhV+F3NBb9oGCNqNON8Cg4nP3/7NOwx412SL6JJUclYjAKmqNtbL6xBfG2RcG0uZWUS/TEHRy4AFLeq5Zg==} requiresBuild: true dependencies: bindings: 1.5.0 @@ -2695,8 +2701,8 @@ packages: dev: true optional: true - /esbuild-android-64/0.15.18: - resolution: {integrity: sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==} + /esbuild-android-64/0.15.16: + resolution: {integrity: sha512-Vwkv/sT0zMSgPSVO3Jlt1pUbnZuOgtOQJkJkyyJFAlLe7BiT8e9ESzo0zQSx4c3wW4T6kGChmKDPMbWTgtliQA==} engines: {node: '>=12'} cpu: [x64] os: [android] @@ -2722,8 +2728,8 @@ packages: dev: true optional: true - /esbuild-android-arm64/0.15.18: - resolution: {integrity: sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==} + /esbuild-android-arm64/0.15.16: + resolution: {integrity: sha512-lqfKuofMExL5niNV3gnhMUYacSXfsvzTa/58sDlBET/hCOG99Zmeh+lz6kvdgvGOsImeo6J9SW21rFCogNPLxg==} engines: {node: '>=12'} cpu: [arm64] os: [android] @@ -2749,8 +2755,8 @@ packages: dev: true optional: true - /esbuild-darwin-64/0.15.18: - resolution: {integrity: sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==} + /esbuild-darwin-64/0.15.16: + resolution: {integrity: sha512-wo2VWk/n/9V2TmqUZ/KpzRjCEcr00n7yahEdmtzlrfQ3lfMCf3Wa+0sqHAbjk3C6CKkR3WKK/whkMq5Gj4Da9g==} engines: {node: '>=12'} cpu: [x64] os: [darwin] @@ -2776,8 +2782,8 @@ packages: dev: true optional: true - /esbuild-darwin-arm64/0.15.18: - resolution: {integrity: sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==} + /esbuild-darwin-arm64/0.15.16: + resolution: {integrity: sha512-fMXaUr5ou0M4WnewBKsspMtX++C1yIa3nJ5R2LSbLCfJT3uFdcRoU/NZjoM4kOMKyOD9Sa/2vlgN8G07K3SJnw==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] @@ -2803,8 +2809,8 @@ packages: dev: true optional: true - /esbuild-freebsd-64/0.15.18: - resolution: {integrity: sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==} + /esbuild-freebsd-64/0.15.16: + resolution: {integrity: sha512-UzIc0xlRx5x9kRuMr+E3+hlSOxa/aRqfuMfiYBXu2jJ8Mzej4lGL7+o6F5hzhLqWfWm1GWHNakIdlqg1ayaTNQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] @@ -2830,8 +2836,8 @@ packages: dev: true optional: true - /esbuild-freebsd-arm64/0.15.18: - resolution: {integrity: sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==} + /esbuild-freebsd-arm64/0.15.16: + resolution: {integrity: sha512-8xyiYuGc0DLZphFQIiYaLHlfoP+hAN9RHbE+Ibh8EUcDNHAqbQgUrQg7pE7Bo00rXmQ5Ap6KFgcR0b4ALZls1g==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] @@ -2857,8 +2863,8 @@ packages: dev: true optional: true - /esbuild-linux-32/0.15.18: - resolution: {integrity: sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==} + /esbuild-linux-32/0.15.16: + resolution: {integrity: sha512-iGijUTV+0kIMyUVoynK0v+32Oi8yyp0xwMzX69GX+5+AniNy/C/AL1MjFTsozRp/3xQPl7jVux/PLe2ds10/2w==} engines: {node: '>=12'} cpu: [ia32] os: [linux] @@ -2884,8 +2890,8 @@ packages: dev: true optional: true - /esbuild-linux-64/0.15.18: - resolution: {integrity: sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==} + /esbuild-linux-64/0.15.16: + resolution: {integrity: sha512-tuSOjXdLw7VzaUj89fIdAaQT7zFGbKBcz4YxbWrOiXkwscYgE7HtTxUavreBbnRkGxKwr9iT/gmeJWNm4djy/g==} engines: {node: '>=12'} cpu: [x64] os: [linux] @@ -2911,8 +2917,8 @@ packages: dev: true optional: true - /esbuild-linux-arm/0.15.18: - resolution: {integrity: sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==} + /esbuild-linux-arm/0.15.16: + resolution: {integrity: sha512-XKcrxCEXDTOuoRj5l12tJnkvuxXBMKwEC5j0JISw3ziLf0j4zIwXbKbTmUrKFWbo6ZgvNpa7Y5dnbsjVvH39bQ==} engines: {node: '>=12'} cpu: [arm] os: [linux] @@ -2938,8 +2944,8 @@ packages: dev: true optional: true - /esbuild-linux-arm64/0.15.18: - resolution: {integrity: sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==} + /esbuild-linux-arm64/0.15.16: + resolution: {integrity: sha512-mPYksnfHnemNrvjrDhZyixL/AfbJN0Xn9S34ZOHYdh6/jJcNd8iTsv3JwJoEvTJqjMggjMhGUPJAdjnFBHoH8A==} engines: {node: '>=12'} cpu: [arm64] os: [linux] @@ -2965,8 +2971,8 @@ packages: dev: true optional: true - /esbuild-linux-mips64le/0.15.18: - resolution: {integrity: sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==} + /esbuild-linux-mips64le/0.15.16: + resolution: {integrity: sha512-kSJO2PXaxfm0pWY39+YX+QtpFqyyrcp0ZeI8QPTrcFVQoWEPiPVtOfTZeS3ZKedfH+Ga38c4DSzmKMQJocQv6A==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] @@ -2992,8 +2998,8 @@ packages: dev: true optional: true - /esbuild-linux-ppc64le/0.15.18: - resolution: {integrity: sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==} + /esbuild-linux-ppc64le/0.15.16: + resolution: {integrity: sha512-NimPikwkBY0yGABw6SlhKrtT35sU4O23xkhlrTT/O6lSxv3Pm5iSc6OYaqVAHWkLdVf31bF4UDVFO+D990WpAA==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] @@ -3019,8 +3025,8 @@ packages: dev: true optional: true - /esbuild-linux-riscv64/0.15.18: - resolution: {integrity: sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==} + /esbuild-linux-riscv64/0.15.16: + resolution: {integrity: sha512-ty2YUHZlwFOwp7pR+J87M4CVrXJIf5ZZtU/umpxgVJBXvWjhziSLEQxvl30SYfUPq0nzeWKBGw5i/DieiHeKfw==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] @@ -3046,8 +3052,8 @@ packages: dev: true optional: true - /esbuild-linux-s390x/0.15.18: - resolution: {integrity: sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==} + /esbuild-linux-s390x/0.15.16: + resolution: {integrity: sha512-VkZaGssvPDQtx4fvVdZ9czezmyWyzpQhEbSNsHZZN0BHvxRLOYAQ7sjay8nMQwYswP6O2KlZluRMNPYefFRs+w==} engines: {node: '>=12'} cpu: [s390x] os: [linux] @@ -3073,8 +3079,8 @@ packages: dev: true optional: true - /esbuild-netbsd-64/0.15.18: - resolution: {integrity: sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==} + /esbuild-netbsd-64/0.15.16: + resolution: {integrity: sha512-ElQ9rhdY51et6MJTWrCPbqOd/YuPowD7Cxx3ee8wlmXQQVW7UvQI6nSprJ9uVFQISqSF5e5EWpwWqXZsECLvXg==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] @@ -3100,8 +3106,8 @@ packages: dev: true optional: true - /esbuild-openbsd-64/0.15.18: - resolution: {integrity: sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==} + /esbuild-openbsd-64/0.15.16: + resolution: {integrity: sha512-KgxMHyxMCT+NdLQE1zVJEsLSt2QQBAvJfmUGDmgEq8Fvjrf6vSKB00dVHUEDKcJwMID6CdgCpvYNt999tIYhqA==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] @@ -3127,8 +3133,8 @@ packages: dev: true optional: true - /esbuild-sunos-64/0.15.18: - resolution: {integrity: sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==} + /esbuild-sunos-64/0.15.16: + resolution: {integrity: sha512-exSAx8Phj7QylXHlMfIyEfNrmqnLxFqLxdQF6MBHPdHAjT7fsKaX6XIJn+aQEFiOcE4X8e7VvdMCJ+WDZxjSRQ==} engines: {node: '>=12'} cpu: [x64] os: [sunos] @@ -3154,8 +3160,8 @@ packages: dev: true optional: true - /esbuild-windows-32/0.15.18: - resolution: {integrity: sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==} + /esbuild-windows-32/0.15.16: + resolution: {integrity: sha512-zQgWpY5pUCSTOwqKQ6/vOCJfRssTvxFuEkpB4f2VUGPBpdddZfdj8hbZuFRdZRPIVHvN7juGcpgCA/XCF37mAQ==} engines: {node: '>=12'} cpu: [ia32] os: [win32] @@ -3181,8 +3187,8 @@ packages: dev: true optional: true - /esbuild-windows-64/0.15.18: - resolution: {integrity: sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==} + /esbuild-windows-64/0.15.16: + resolution: {integrity: sha512-HjW1hHRLSncnM3MBCP7iquatHVJq9l0S2xxsHHj4yzf4nm9TU4Z7k4NkeMlD/dHQ4jPlQQhwcMvwbJiOefSuZw==} engines: {node: '>=12'} cpu: [x64] os: [win32] @@ -3208,8 +3214,8 @@ packages: dev: true optional: true - /esbuild-windows-arm64/0.15.18: - resolution: {integrity: sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==} + /esbuild-windows-arm64/0.15.16: + resolution: {integrity: sha512-oCcUKrJaMn04Vxy9Ekd8x23O8LoU01+4NOkQ2iBToKgnGj5eo1vU9i27NQZ9qC8NFZgnQQZg5oZWAejmbsppNA==} engines: {node: '>=12'} cpu: [arm64] os: [win32] @@ -3273,34 +3279,34 @@ packages: esbuild-windows-arm64: 0.14.51 dev: true - /esbuild/0.15.18: - resolution: {integrity: sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==} + /esbuild/0.15.16: + resolution: {integrity: sha512-o6iS9zxdHrrojjlj6pNGC2NAg86ECZqIETswTM5KmJitq+R1YmahhWtMumeQp9lHqJaROGnsBi2RLawGnfo5ZQ==} engines: {node: '>=12'} hasBin: true requiresBuild: true optionalDependencies: - '@esbuild/android-arm': 0.15.18 - '@esbuild/linux-loong64': 0.15.18 - esbuild-android-64: 0.15.18 - esbuild-android-arm64: 0.15.18 - esbuild-darwin-64: 0.15.18 - esbuild-darwin-arm64: 0.15.18 - esbuild-freebsd-64: 0.15.18 - esbuild-freebsd-arm64: 0.15.18 - esbuild-linux-32: 0.15.18 - esbuild-linux-64: 0.15.18 - esbuild-linux-arm: 0.15.18 - esbuild-linux-arm64: 0.15.18 - esbuild-linux-mips64le: 0.15.18 - esbuild-linux-ppc64le: 0.15.18 - esbuild-linux-riscv64: 0.15.18 - esbuild-linux-s390x: 0.15.18 - esbuild-netbsd-64: 0.15.18 - esbuild-openbsd-64: 0.15.18 - esbuild-sunos-64: 0.15.18 - esbuild-windows-32: 0.15.18 - esbuild-windows-64: 0.15.18 - esbuild-windows-arm64: 0.15.18 + '@esbuild/android-arm': 0.15.16 + '@esbuild/linux-loong64': 0.15.16 + esbuild-android-64: 0.15.16 + esbuild-android-arm64: 0.15.16 + esbuild-darwin-64: 0.15.16 + esbuild-darwin-arm64: 0.15.16 + esbuild-freebsd-64: 0.15.16 + esbuild-freebsd-arm64: 0.15.16 + esbuild-linux-32: 0.15.16 + esbuild-linux-64: 0.15.16 + esbuild-linux-arm: 0.15.16 + esbuild-linux-arm64: 0.15.16 + esbuild-linux-mips64le: 0.15.16 + esbuild-linux-ppc64le: 0.15.16 + esbuild-linux-riscv64: 0.15.16 + esbuild-linux-s390x: 0.15.16 + esbuild-netbsd-64: 0.15.16 + esbuild-openbsd-64: 0.15.16 + esbuild-sunos-64: 0.15.16 + esbuild-windows-32: 0.15.16 + esbuild-windows-64: 0.15.16 + esbuild-windows-arm64: 0.15.16 dev: true /escalade/3.1.1: @@ -3325,16 +3331,16 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - /eslint-config-prettier/8.5.0_eslint@8.29.0: + /eslint-config-prettier/8.5.0_eslint@8.28.0: resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 8.29.0 + eslint: 8.28.0 dev: true - /eslint-config-standard-with-typescript/23.0.0_2xnbfsx6e3yf6rmssbfxbc3uau: + /eslint-config-standard-with-typescript/23.0.0_lwp7uu2ndga6sqsuliw5ylzib4: resolution: {integrity: sha512-iaaWifImn37Z1OXbNW1es7KI+S7D408F9ys0bpaQf2temeBWlvb0Nc5qHkOgYaRb5QxTZT32GGeN1gtswASOXA==} peerDependencies: '@typescript-eslint/eslint-plugin': ^5.0.0 @@ -3344,19 +3350,19 @@ packages: eslint-plugin-promise: ^6.0.0 typescript: '*' dependencies: - '@typescript-eslint/eslint-plugin': 5.45.0_yjegg5cyoezm3fzsmuszzhetym - '@typescript-eslint/parser': 5.45.0_s5ps7njkmjlaqajutnox5ntcla - eslint: 8.29.0 - eslint-config-standard: 17.0.0_wnkmxhw54rcoqx42l6oqxte7qq - eslint-plugin-import: 2.26.0_ub3senzxbs32f65wl7xoyha6lu - eslint-plugin-n: 15.6.0_eslint@8.29.0 - eslint-plugin-promise: 6.1.1_eslint@8.29.0 + '@typescript-eslint/eslint-plugin': 5.45.0_czs5uoqkd3podpy6vgtsxfc7au + '@typescript-eslint/parser': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a + eslint: 8.28.0 + eslint-config-standard: 17.0.0_5dakk4wnrkkieagghiqvu5yn4y + eslint-plugin-import: 2.26.0_vbnhqcxlbs7ynbxw44hu2vq7eq + eslint-plugin-n: 15.5.1_eslint@8.28.0 + eslint-plugin-promise: 6.1.1_eslint@8.28.0 typescript: 4.9.3 transitivePeerDependencies: - supports-color dev: true - /eslint-config-standard/17.0.0_wnkmxhw54rcoqx42l6oqxte7qq: + /eslint-config-standard/17.0.0_5dakk4wnrkkieagghiqvu5yn4y: resolution: {integrity: sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==} peerDependencies: eslint: ^8.0.1 @@ -3364,20 +3370,20 @@ packages: eslint-plugin-n: ^15.0.0 eslint-plugin-promise: ^6.0.0 dependencies: - eslint: 8.29.0 - eslint-plugin-import: 2.26.0_ub3senzxbs32f65wl7xoyha6lu - eslint-plugin-n: 15.6.0_eslint@8.29.0 - eslint-plugin-promise: 6.1.1_eslint@8.29.0 + eslint: 8.28.0 + eslint-plugin-import: 2.26.0_vbnhqcxlbs7ynbxw44hu2vq7eq + eslint-plugin-n: 15.5.1_eslint@8.28.0 + eslint-plugin-promise: 6.1.1_eslint@8.28.0 dev: true - /eslint-etc/5.2.0_s5ps7njkmjlaqajutnox5ntcla: + /eslint-etc/5.2.0_hsf322ms6xhhd4b5ne6lb74y4a: resolution: {integrity: sha512-Gcm/NMa349FOXb1PEEfNMMyIANuorIc2/mI5Vfu1zENNsz+FBVhF62uY6gPUCigm/xDOc8JOnl+71WGnlzlDag==} peerDependencies: eslint: ^8.0.0 typescript: ^4.0.0 dependencies: - '@typescript-eslint/experimental-utils': 5.45.0_s5ps7njkmjlaqajutnox5ntcla - eslint: 8.29.0 + '@typescript-eslint/experimental-utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a + eslint: 8.28.0 tsutils: 3.21.0_typescript@4.9.3 tsutils-etc: 1.4.1_6srv2tajnzf4k7zj2br63blj3e typescript: 4.9.3 @@ -3394,7 +3400,7 @@ packages: - supports-color dev: true - /eslint-module-utils/2.7.4_ka2zl4kbfnnb6pzn3mgzpmhlt4: + /eslint-module-utils/2.7.4_kr6tb4mi2cmpd7whrqyyy67tyi: resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} peerDependencies: @@ -3415,35 +3421,35 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.45.0_s5ps7njkmjlaqajutnox5ntcla + '@typescript-eslint/parser': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a debug: 3.2.7 - eslint: 8.29.0 + eslint: 8.28.0 eslint-import-resolver-node: 0.3.6 transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-es/4.1.0_eslint@8.29.0: + /eslint-plugin-es/4.1.0_eslint@8.28.0: resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} engines: {node: '>=8.10.0'} peerDependencies: eslint: '>=4.19.1' dependencies: - eslint: 8.29.0 + eslint: 8.28.0 eslint-utils: 2.1.0 regexpp: 3.2.0 dev: true - /eslint-plugin-etc/2.0.2_s5ps7njkmjlaqajutnox5ntcla: + /eslint-plugin-etc/2.0.2_hsf322ms6xhhd4b5ne6lb74y4a: resolution: {integrity: sha512-g3b95LCdTCwZA8On9EICYL8m1NMWaiGfmNUd/ftZTeGZDXrwujKXUr+unYzqKjKFo1EbqJ31vt+Dqzrdm/sUcw==} peerDependencies: eslint: ^8.0.0 typescript: ^4.0.0 dependencies: '@phenomnomnominal/tsquery': 4.2.0_typescript@4.9.3 - '@typescript-eslint/experimental-utils': 5.45.0_s5ps7njkmjlaqajutnox5ntcla - eslint: 8.29.0 - eslint-etc: 5.2.0_s5ps7njkmjlaqajutnox5ntcla + '@typescript-eslint/experimental-utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a + eslint: 8.28.0 + eslint-etc: 5.2.0_hsf322ms6xhhd4b5ne6lb74y4a requireindex: 1.2.0 tslib: 2.4.1 tsutils: 3.21.0_typescript@4.9.3 @@ -3452,7 +3458,7 @@ packages: - supports-color dev: true - /eslint-plugin-import/2.26.0_ub3senzxbs32f65wl7xoyha6lu: + /eslint-plugin-import/2.26.0_vbnhqcxlbs7ynbxw44hu2vq7eq: resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==} engines: {node: '>=4'} peerDependencies: @@ -3462,14 +3468,14 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.45.0_s5ps7njkmjlaqajutnox5ntcla + '@typescript-eslint/parser': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a array-includes: 3.1.6 array.prototype.flat: 1.3.1 debug: 2.6.9 doctrine: 2.1.0 - eslint: 8.29.0 + eslint: 8.28.0 eslint-import-resolver-node: 0.3.6 - eslint-module-utils: 2.7.4_ka2zl4kbfnnb6pzn3mgzpmhlt4 + eslint-module-utils: 2.7.4_kr6tb4mi2cmpd7whrqyyy67tyi has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 @@ -3483,7 +3489,7 @@ packages: - supports-color dev: true - /eslint-plugin-jsdoc/39.6.4_eslint@8.29.0: + /eslint-plugin-jsdoc/39.6.4_eslint@8.28.0: resolution: {integrity: sha512-fskvdLCfwmPjHb6e+xNGDtGgbF8X7cDwMtVLAP2WwSf9Htrx68OAx31BESBM1FAwsN2HTQyYQq7m4aW4Q4Nlag==} engines: {node: ^14 || ^16 || ^17 || ^18 || ^19} peerDependencies: @@ -3493,7 +3499,7 @@ packages: comment-parser: 1.3.1 debug: 4.3.4 escape-string-regexp: 4.0.0 - eslint: 8.29.0 + eslint: 8.28.0 esquery: 1.4.0 semver: 7.3.8 spdx-expression-parse: 3.0.1 @@ -3501,16 +3507,16 @@ packages: - supports-color dev: true - /eslint-plugin-n/15.6.0_eslint@8.29.0: - resolution: {integrity: sha512-Hd/F7wz4Mj44Jp0H6Jtty13NcE69GNTY0rVlgTIj1XBnGGVI6UTdDrpE6vqu3AHo07bygq/N+7OH/lgz1emUJw==} + /eslint-plugin-n/15.5.1_eslint@8.28.0: + resolution: {integrity: sha512-kAd+xhZm7brHoFLzKLB7/FGRFJNg/srmv67mqb7tto22rpr4wv/LV6RuXzAfv3jbab7+k1wi42PsIhGviywaaw==} engines: {node: '>=12.22.0'} peerDependencies: eslint: '>=7.0.0' dependencies: builtins: 5.0.1 - eslint: 8.29.0 - eslint-plugin-es: 4.1.0_eslint@8.29.0 - eslint-utils: 3.0.0_eslint@8.29.0 + eslint: 8.28.0 + eslint-plugin-es: 4.1.0_eslint@8.28.0 + eslint-utils: 3.0.0_eslint@8.28.0 ignore: 5.2.1 is-core-module: 2.11.0 minimatch: 3.1.2 @@ -3523,25 +3529,25 @@ packages: engines: {node: '>=5.0.0'} dev: true - /eslint-plugin-promise/6.1.1_eslint@8.29.0: + /eslint-plugin-promise/6.1.1_eslint@8.28.0: resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - eslint: 8.29.0 + eslint: 8.28.0 dev: true - /eslint-plugin-react-hooks/4.6.0_eslint@8.29.0: + /eslint-plugin-react-hooks/4.6.0_eslint@8.28.0: resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 dependencies: - eslint: 8.29.0 + eslint: 8.28.0 dev: true - /eslint-plugin-react/7.31.11_eslint@8.29.0: + /eslint-plugin-react/7.31.11_eslint@8.28.0: resolution: {integrity: sha512-TTvq5JsT5v56wPa9OYHzsrOlHzKZKjV+aLgS+55NJP/cuzdiQPC7PfYoUjMoxlffKtvijpk7vA/jmuqRb9nohw==} engines: {node: '>=4'} peerDependencies: @@ -3551,7 +3557,7 @@ packages: array.prototype.flatmap: 1.3.1 array.prototype.tosorted: 1.1.1 doctrine: 2.1.0 - eslint: 8.29.0 + eslint: 8.28.0 estraverse: 5.3.0 jsx-ast-utils: 3.3.3 minimatch: 3.1.2 @@ -3565,7 +3571,7 @@ packages: string.prototype.matchall: 4.0.8 dev: true - /eslint-plugin-unicorn/44.0.2_eslint@8.29.0: + /eslint-plugin-unicorn/44.0.2_eslint@8.28.0: resolution: {integrity: sha512-GLIDX1wmeEqpGaKcnMcqRvMVsoabeF0Ton0EX4Th5u6Kmf7RM9WBl705AXFEsns56ESkEs0uyelLuUTvz9Tr0w==} engines: {node: '>=14.18'} peerDependencies: @@ -3574,8 +3580,8 @@ packages: '@babel/helper-validator-identifier': 7.19.1 ci-info: 3.7.0 clean-regexp: 1.0.0 - eslint: 8.29.0 - eslint-utils: 3.0.0_eslint@8.29.0 + eslint: 8.28.0 + eslint-utils: 3.0.0_eslint@8.28.0 esquery: 1.4.0 indent-string: 4.0.0 is-builtin-module: 3.2.0 @@ -3611,13 +3617,13 @@ packages: eslint-visitor-keys: 1.3.0 dev: true - /eslint-utils/3.0.0_eslint@8.29.0: + /eslint-utils/3.0.0_eslint@8.28.0: resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} peerDependencies: eslint: '>=5' dependencies: - eslint: 8.29.0 + eslint: 8.28.0 eslint-visitor-keys: 2.1.0 dev: true @@ -3636,8 +3642,8 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint/8.29.0: - resolution: {integrity: sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==} + /eslint/8.28.0: + resolution: {integrity: sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: @@ -3652,7 +3658,7 @@ packages: doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.1.1 - eslint-utils: 3.0.0_eslint@8.29.0 + eslint-utils: 3.0.0_eslint@8.28.0 eslint-visitor-keys: 3.3.0 espree: 9.4.1 esquery: 1.4.0 @@ -3813,8 +3819,8 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true - /fastq/1.14.0: - resolution: {integrity: sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==} + /fastq/1.13.0: + resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} dependencies: reusify: 1.0.4 dev: true @@ -4135,22 +4141,22 @@ packages: resolution: {integrity: sha512-Pg7yAvasl8QRsymK/m0S184fDPiz03/VOAdbRbHdsaFcp41drGh9NdzG2jLJHpRuNoWxL/bH0R4kSULT6DvpSw==} engines: {node: '>=14'} dependencies: - '@typescript-eslint/eslint-plugin': 5.45.0_yjegg5cyoezm3fzsmuszzhetym - '@typescript-eslint/parser': 5.45.0_s5ps7njkmjlaqajutnox5ntcla - eslint: 8.29.0 - eslint-config-prettier: 8.5.0_eslint@8.29.0 - eslint-config-standard: 17.0.0_wnkmxhw54rcoqx42l6oqxte7qq - eslint-config-standard-with-typescript: 23.0.0_2xnbfsx6e3yf6rmssbfxbc3uau - eslint-plugin-etc: 2.0.2_s5ps7njkmjlaqajutnox5ntcla - eslint-plugin-import: 2.26.0_ub3senzxbs32f65wl7xoyha6lu - eslint-plugin-jsdoc: 39.6.4_eslint@8.29.0 - eslint-plugin-n: 15.6.0_eslint@8.29.0 + '@typescript-eslint/eslint-plugin': 5.45.0_czs5uoqkd3podpy6vgtsxfc7au + '@typescript-eslint/parser': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a + eslint: 8.28.0 + eslint-config-prettier: 8.5.0_eslint@8.28.0 + eslint-config-standard: 17.0.0_5dakk4wnrkkieagghiqvu5yn4y + eslint-config-standard-with-typescript: 23.0.0_lwp7uu2ndga6sqsuliw5ylzib4 + eslint-plugin-etc: 2.0.2_hsf322ms6xhhd4b5ne6lb74y4a + eslint-plugin-import: 2.26.0_vbnhqcxlbs7ynbxw44hu2vq7eq + eslint-plugin-jsdoc: 39.6.4_eslint@8.28.0 + eslint-plugin-n: 15.5.1_eslint@8.28.0 eslint-plugin-no-only-tests: 3.1.0 - eslint-plugin-promise: 6.1.1_eslint@8.29.0 - eslint-plugin-react: 7.31.11_eslint@8.29.0 - eslint-plugin-react-hooks: 4.6.0_eslint@8.29.0 - eslint-plugin-unicorn: 44.0.2_eslint@8.29.0 - lint-staged: 13.1.0 + eslint-plugin-promise: 6.1.1_eslint@8.28.0 + eslint-plugin-react: 7.31.11_eslint@8.28.0 + eslint-plugin-react-hooks: 4.6.0_eslint@8.28.0 + eslint-plugin-unicorn: 44.0.2_eslint@8.28.0 + lint-staged: 13.0.4 prettier: 2.8.0 simple-git-hooks: 2.8.1 typescript: 4.9.3 @@ -4277,7 +4283,7 @@ packages: mute-stream: 0.0.8 ora: 6.1.2 run-async: 2.4.1 - rxjs: 7.6.0 + rxjs: 7.5.7 string-width: 5.1.2 strip-ansi: 7.0.1 through: 2.3.8 @@ -4529,6 +4535,10 @@ packages: has-tostringtag: 1.0.0 dev: true + /is-subset/0.1.1: + resolution: {integrity: sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==} + dev: true + /is-symbol/1.0.4: resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} engines: {node: '>= 0.4'} @@ -4783,8 +4793,8 @@ packages: /lines-and-columns/1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - /lint-staged/13.1.0: - resolution: {integrity: sha512-pn/sR8IrcF/T0vpWLilih8jmVouMlxqXxKuAojmbiGX5n/gDnz+abdPptlj0vYnbfE0SQNl3CY/HwtM0+yfOVQ==} + /lint-staged/13.0.4: + resolution: {integrity: sha512-HxlHCXoYRsq9QCby5wFozmZW00hMs/9e3l+/dz6Qr8Kle4UH0kJTdABAbqhzG+3pcG6QjL9kz7NgGBfph+a5dw==} engines: {node: ^14.13.1 || >=16.0.0} hasBin: true dependencies: @@ -4820,7 +4830,7 @@ packages: log-update: 4.0.0 p-map: 4.0.0 rfdc: 1.3.0 - rxjs: 7.6.0 + rxjs: 7.5.7 through: 2.3.8 wrap-ansi: 7.0.0 dev: true @@ -6183,8 +6193,8 @@ packages: queue-microtask: 1.2.3 dev: true - /rxjs/7.6.0: - resolution: {integrity: sha512-DDa7d8TFNUalGC9VqXvQ1euWNN7sc63TrUCuM9J998+ViviahMIjKSOU7rfcgFOF+FCD71BhDRv4hrFz+ImDLQ==} + /rxjs/7.5.7: + resolution: {integrity: sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==} dependencies: tslib: 2.4.1 @@ -6435,7 +6445,6 @@ packages: /sourcemap-codec/1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} - deprecated: Please use @jridgewell/sourcemap-codec instead /sparse-array/1.3.2: resolution: {integrity: sha512-ZT711fePGn3+kQyLuv1fpd3rNSkNF8vd5Kv2D+qnOANeyKs3fx6bUMGWRPvgTTcYV64QMqZKZwcuaQSP3AZ0tg==} @@ -6883,7 +6892,6 @@ packages: /type-fest/3.3.0: resolution: {integrity: sha512-gezeeOIZyQLGW5uuCeEnXF1aXmtt2afKspXz3YqoOcZ3l/YMJq1pujvgT+cz/Nw1O/7q/kSav5fihJHsC/AOUg==} engines: {node: '>=14.16'} - dev: false /typedoc-plugin-missing-exports/1.0.0_typedoc@0.23.21: resolution: {integrity: sha512-7s6znXnuAj1eD9KYPyzVzR1lBF5nwAY8IKccP5sdoO9crG4lpd16RoFpLsh2PccJM+I2NASpr0+/NMka6ThwVA==}