diff --git a/caps.md b/caps.md new file mode 100644 index 000000000..2911c5b15 --- /dev/null +++ b/caps.md @@ -0,0 +1,90 @@ +# Capabilities + +```mermaid +erDiagram + consumer { + subscription TEXT "Subscription id" + consumer DID "Space DID" + } + + subscription { + id TEXT "${order}@${provider}" + order TEXT "Unique identifier" + provider DID "Provider DID" + customer DID "Account DID" + } + + subscription ||--|{ consumer : consumer +``` + +> Perhaps we do not need two tables here ? iIt is 1:n relationship but what is the benefit over just adding `consumer` field to the `subscription` ? +> + +## `access/authorize` + +When invoked we can check the `subscription` table and insert a record like + +```js +{ + provider: "did:web:web3.storage", + customer: "did:mailto:web.mail:alice", + order: CBOR.link({ customer }) +} +``` + + +## `consumer/add` + +Delegated by the provider to the account + +```json +{ + "iss": "did:web:web3.storage", + "aud": "did:mailto:web.mail:alice", + "att": [{ + "with": "did:web:web3.storage", + "can": "consumer/*", + "nb": { + "customer": "did:mailto:web.mail:alice", + "order": "bafy...hash" + } + }] +} +``` + +This capability set allows invoker: + +1. Insert into `subscription` table record where + - `provider` is `with` + - `customer` is `nb.customer` + - `order` is `nb.order` + +2. Insert into `consumer` table records where +- `subscription` is `${nb.order}@${with}` +- `consumer` is `*` + +> Provider MAY want to enforce some constraints like limit the number of consumers but that is out of scope for now. + +## `consumer/remove` + +Delegated by the provider to the account + +This capability allows invoker to delete records from the `consumer` table. + + +# Provider is in charge + +Because provider is doing the delegation it can also invoke any of the capabilities and revoke capabilities it issued. + + +## `provider/get` + +> Do we even need it ?? + +It is a way for to request a `consumer/*` capability delegation from the `provider`. + +## `provider/add` + +> Do we even need it ?? + +It is a way to request a `consumer/add` without having to do `provider/get` first. Because `provider` can invoke `consumer/add` this just short circuits. diff --git a/package.json b/package.json index 22866c270..e5390e498 100644 --- a/package.json +++ b/package.json @@ -21,18 +21,10 @@ "docusaurus-plugin-typedoc": "^0.18.0", "lint-staged": "^13.1.0", "prettier": "2.8.3", - "simple-git-hooks": "^2.8.1", "typedoc-plugin-markdown": "^3.14.0", "typescript": "4.9.5", "wrangler": "^2.8.0" }, - "simple-git-hooks": { - "pre-commit": "npx lint-staged" - }, - "lint-staged": { - "*.{js,ts,yml,json}": "prettier --write", - "*.js": "eslint --fix" - }, "prettier": { "trailingComma": "es5", "tabWidth": 2, diff --git a/packages/access-api/migrations/0007_add_provider_contracts.sql b/packages/access-api/migrations/0007_add_provider_contracts.sql new file mode 100644 index 000000000..afd5e2233 --- /dev/null +++ b/packages/access-api/migrations/0007_add_provider_contracts.sql @@ -0,0 +1,74 @@ +-- Migration number: 0007 2023-03-10T14:14:00.000Z + +-- goal: add tables to keep track of the subscriptions accounts have with +-- providers and a table to keep track of consumers of the subscriptions. + +-- Table is used to keep track of the accounts subscribed to a provider(s). +-- Insertion here are caused by `customer/add` capability invocation. +-- Records here are idempotent meaning that invoking `customer/add` for the +-- same (order, provider, customer) triple will have no effect. +CREATE TABLE + IF NOT EXISTS subscriptions ( + -- CID of the `customer/*` delegation from provider to the customer. + provision TEXT NOT NULL, + -- CID of the Task that created this subscription, usually this would be + -- `customer/add` invocation. + cause TEXT NOT NULL, + -- Unique identifier for this subscription + order TEXT NOT NULL, + -- DID of the provider e.g. a storage provider + provider TEXT NOT NULL, + -- DID of the customer + customer TEXT NOT NULL, + -- metadata + inserted_at TEXT NOT NULL DEFAULT (strftime ('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime ('%Y-%m-%dT%H:%M:%fZ', 'now')), + + -- Operation is idempotent, so we'll have a CID of the task that created + -- this subscription. All subsequent invocations will be NOOPs. + CONSTRAINT task_cid UNIQUE (cause), + -- Subscription ID is derived from (order, provider) and is unique. + -- Note that `customer` is not part of the primary key because we want to + -- allow provider to choose how to enforce uniqueness constraint using + -- the `order` field. + PRIMARY KEY (order, provider) + ) + +-- Table is used to keep track of the consumers of a subscription. Insertion +-- is caused by `customer/add` capability invocation typically by the account +-- that has been delegated `customer/*` capability when subscription was +-- created. +-- Note that while this table has a superset of the columns of `subscription` +-- table wi still need both because consumers may be added and removed without +-- canceling the subscription. +CREATE TABLE + IF NOT EXISTS consumers ( + -- CID of the invocation that created this subscription + cause TEXT NOT NULL, + + -- Below fields are used only to derive subscription ID. + -- Unique identifier for this subscription + order TEXT NOT NULL, + -- DID of the provider e.g. a storage provider + provider TEXT NOT NULL, + + -- subscription ID is derived from (order, provider). This is a virtual + -- column which is not stored in the database but could be used in queries. + subscription TEXT GENERATED ALWAYS AS (format("%s@%s", order, provider)) VIRTUAL, + + -- consumer DID + consumer TEXT NOT NULL, + -- metadata + inserted_at TEXT NOT NULL DEFAULT (strftime ('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime ('%Y-%m-%dT%H:%M:%fZ', 'now')), + + -- Operation that caused insertion of this record. + CONSTRAINT task_cid UNIQUE (cause), + -- We use (order, provider, consumer) as a composite primary key to enforce + -- uniqueness constraint from the provider side. This allows provider to + -- decide how to generate the order ID to enforce whatever constraint they + -- want. E.g. web3.storage will enforce one provider per space by generating + -- order by account DID, while nft.storage may choose to not have such + -- limitation and generate a unique order based on other factors. + PRIMARY KEY (order, provider, consumer) + ); diff --git a/packages/access-api/package.json b/packages/access-api/package.json index 18f23566c..beed5a9f1 100644 --- a/packages/access-api/package.json +++ b/packages/access-api/package.json @@ -90,6 +90,8 @@ }, "rules": { "unicorn/prefer-number-properties": "off", + "@typescript-eslint/ban-types": "off", + "unicorn/prefer-export-from": "off", "jsdoc/no-undefined-types": [ "error", { diff --git a/packages/access-api/src/bindings.d.ts b/packages/access-api/src/bindings.d.ts index 04cc64515..40c524ff6 100644 --- a/packages/access-api/src/bindings.d.ts +++ b/packages/access-api/src/bindings.d.ts @@ -6,12 +6,16 @@ import type { } from '@web3-storage/access/types' import type { Handler as _Handler } from '@web3-storage/worker-utils/router' import { Spaces } from './models/spaces.js' -import { Validations } from './models/validations.js' import { loadConfig } from './config.js' +import type { DID } from '@ucanto/interface' import { ConnectionView, Signer as EdSigner } from '@ucanto/principal/ed25519' -import { Accounts } from './models/accounts.js' -import { DelegationsStorage as Delegations } from './types/delegations.js' -import { ProvisionsStorage } from './types/provisions.js' +import { + DelegationStore, + ProvisionStore, + ConsumerStore, + ValidationStore, + AccountStore, +} from './types/index.js' export {} @@ -26,8 +30,12 @@ export interface AnalyticsEngineEvent { } export interface Email { - sendValidation: ({ to: string, url: string }) => Promise - send: ({ to: string, textBody: string, subject: string }) => Promise + sendValidation: (input: { to: string; url: string }) => Promise + send: (input: { + to: string + textBody: string + subject: string + }) => Promise } export interface Env { @@ -59,16 +67,18 @@ export interface Env { export interface RouteContext { log: Logging - signer: EdSigner.Signer + signer: EdSigner.Signer> config: ReturnType url: URL email: Email models: { - accounts: Accounts - delegations: Delegations + accounts: AccountStore + delegations: DelegationStore spaces: Spaces - provisions: ProvisionsStorage - validations: Validations + provisions: ProvisionStore + validations: ValidationStore + consumers: ConsumerStore + subscriptions: SubscriptionStore } uploadApi: ConnectionView } diff --git a/packages/access-api/src/models/accounts.js b/packages/access-api/src/models/accounts.js index 2feb1dbc5..04886f9a8 100644 --- a/packages/access-api/src/models/accounts.js +++ b/packages/access-api/src/models/accounts.js @@ -3,6 +3,7 @@ import * as Ucanto from '@ucanto/interface' import { Kysely } from 'kysely' import { D1Dialect } from 'kysely-d1' import { GenericPlugin } from '../utils/d1.js' +import * as API from '../types/index.js' /** * @typedef {import('@web3-storage/access/src/types.js').DelegationRecord} DelegationRecord @@ -10,6 +11,7 @@ import { GenericPlugin } from '../utils/d1.js' /** * Accounts + * @implements {API.AccountStore} */ export class Accounts { /** @@ -33,7 +35,7 @@ export class Accounts { } /** - * @param {Ucanto.URI<"did:">} did + * @param {Ucanto.DID} did */ async create(did) { const result = await this.d1 @@ -44,17 +46,20 @@ export class Accounts { .onConflict((oc) => oc.column('did').doNothing()) .returning('accounts.did') .execute() + return { data: result } } /** - * @param {Ucanto.URI<"did:">} did + * @param {Ucanto.DID} did */ async get(did) { - return await this.d1 + const out = await this.d1 .selectFrom('accounts') .selectAll() .where('accounts.did', '=', did) .executeTakeFirst() + + return out } } diff --git a/packages/access-api/src/models/consumers.js b/packages/access-api/src/models/consumers.js new file mode 100644 index 000000000..c40b8a33f --- /dev/null +++ b/packages/access-api/src/models/consumers.js @@ -0,0 +1,172 @@ +import * as API from '../types/consumers.js' +import { D1Dialect } from 'kysely-d1' +import { Kysely } from 'kysely' +import { GenericPlugin, D1Error } from '../utils/d1.js' +import * as Link from '@ucanto/core/link' + +const time = () => /** @type {API.Timestamp} */ (new Date()) + +/** + * @implements {API.ConsumerStore} + */ +export class Consumers { + /** + * @param {object} input + * @param {API.ConsumerRecord[]} input.records + */ + constructor({ records }) { + this.records = records + } + + /** + * @param {API.ConsumerAdd} record + */ + async add({ cause, provider, consumer, order }) { + const matches = await this.find({ provider, consumer }) + for await (const match of matches) { + match.order.toString() + if (match.order.toString() === order.toString()) { + return { cause: match.cause } + } + } + + this.records.push({ + subscription: `${order}@${provider}`, + cause, + provider, + consumer, + order, + inserted_at: time(), + updated_at: time(), + }) + + return { cause } + } + + /** + * @param {Omit} query + */ + async remove({ provider, consumer, order }) { + for (const [offset, record] of this.records.entries()) { + if ( + record.order.toString() === order.toString() && + record.consumer === consumer && + record.provider === provider + ) { + this.records.splice(offset, 1) + + return { cause: record.cause } + } + } + return {} + } + + /** + * @param {API.ConsumerQuery} query + * + */ + async find({ provider, consumer, order, customer }) { + return await this.records.filter((record) => { + return ( + (!provider || record.provider === provider) && + (!consumer || record.consumer === consumer) && + (!order || record.order.toString() === order.toString()) + ) + }) + } +} + +/** + * @implements {API.ConsumerStore} + */ +export class ConsumerDB { + /** + * @param {object} input + * @param {D1Database} input.d1 + */ + constructor({ d1 }) { + /** @type {'consumers'} */ + this.tableName = 'consumers' + /** @type {API.Database<{ consumers: API.Table }>} */ + this.d1 = new Kysely({ + dialect: new D1Dialect({ database: d1 }), + plugins: [ + /** @type {GenericPlugin} */ + new GenericPlugin({ + cause: (v) => Link.parse(v), + inserted_at: (v) => new Date(v), + updated_at: (v) => new Date(v), + }), + ], + }) + } + + /** + * @param {API.ConsumerAdd} record + */ + async add({ cause, provider, consumer, order }) { + try { + return await this.d1 + .insertInto(this.tableName) + .values({ + cause: cause.toString(), + provider, + consumer, + order: order.toString(), + }) + // inserting same record twice with a different cause is a noop and + // we just ignore it. + .onConflict((oc) => oc.constraint('task_cid').doNothing()) + .returning('cause') + .executeTakeFirstOrThrow() + } catch (error) { + return new D1Error( + /** @type {import('../bindings').D1ErrorRaw} */ (error) + ) + } + } + + /** + * @param {API.ConsumerRemove} record + */ + async remove({ provider, consumer, order }) { + try { + const result = await this.d1 + .deleteFrom(this.tableName) + .where('provider', '=', provider) + .where('consumer', '=', consumer) + .where('order', '=', order) + .returning('cause') + .executeTakeFirst() + return result || {} + } catch (error) { + return new D1Error( + /** @type {import('../bindings').D1ErrorRaw} */ (error) + ) + } + } + + /** + * @param {API.ConsumerQuery} query + */ + async find({ provider, consumer, order }) { + let query = this.d1.selectFrom(this.tableName).selectAll() + if (provider) { + query = query.where('consumers.provider', '=', provider) + } + + if (consumer) { + query = query.where('consumers.consumer', '=', consumer) + } + + if (order) { + query = query.where('consumers.order', '=', order) + } + + // if (customer) { + // query = query.where('consumers.customer', '=', customer) + // } + + return await query.execute() + } +} diff --git a/packages/access-api/src/models/delegations.js b/packages/access-api/src/models/delegations.js index b18efa166..998919397 100644 --- a/packages/access-api/src/models/delegations.js +++ b/packages/access-api/src/models/delegations.js @@ -37,9 +37,7 @@ export class DbDelegationsStorage { constructor(db) { this.#db = db // eslint-disable-next-line no-void - void ( - /** @type {import('../types/delegations').DelegationsStorage} */ (this) - ) + void (/** @type {import('../types/delegations').DelegationStore} */ (this)) } async count() { @@ -51,9 +49,9 @@ export class DbDelegationsStorage { } /** - * @param {import('../types/delegations').Query} query + * @param {import('../types/delegations').DelegationQuery} query */ - async *find(query) { + async find(query) { const { audience } = query const { delegations } = this.#tables const selection = await this.#db @@ -61,9 +59,8 @@ export class DbDelegationsStorage { .selectAll() .where(`${delegations}.audience`, '=', audience) .execute() - for await (const row of selection) { - yield rowToDelegation(row) - } + + return selection.map((row) => rowToDelegation(row)) } /** diff --git a/packages/access-api/src/models/provisions.js b/packages/access-api/src/models/provisions.js index ab62340e5..d051ca68b 100644 --- a/packages/access-api/src/models/provisions.js +++ b/packages/access-api/src/models/provisions.js @@ -2,7 +2,7 @@ /** * @template {import("@ucanto/interface").DID} ServiceId - * @typedef {import("../types/provisions").ProvisionsStorage} Provisions + * @typedef {import("../types/provisions").ProvisionStore} Provisions */ /** diff --git a/packages/access-api/src/models/subscriptions.js b/packages/access-api/src/models/subscriptions.js new file mode 100644 index 000000000..aa2e639d8 --- /dev/null +++ b/packages/access-api/src/models/subscriptions.js @@ -0,0 +1,79 @@ +import * as API from '../types/index.js' +import { Kysely } from 'kysely' +import { GenericPlugin, D1Error } from '../utils/d1.js' +import { D1Dialect } from 'kysely-d1' +import * as Link from '@ucanto/core/link' + +/** + * @implements {API.SubscriptionStore} + */ +export class Subscription { + /** + * @param {object} input + * @param {D1Database} input.d1 + */ + constructor({ d1 }) { + /** @type {'subscriptions'} */ + this.tableName = 'subscriptions' + /** @type {API.Database<{ subscriptions: API.Table }>} */ + this.d1 = new Kysely({ + dialect: new D1Dialect({ database: d1 }), + plugins: [ + /** @type {GenericPlugin} */ + new GenericPlugin({ + cause: (v) => Link.parse(v), + inserted_at: (v) => new Date(v), + updated_at: (v) => new Date(v), + }), + ], + }) + } + + /** + * @param {API.Subscription} record + */ + async add({ cause, provider, customer, provision, order }) { + try { + const result = await this.d1 + .insertInto(this.tableName) + .values({ + provision: provision.toString(), + cause: cause.toString(), + provider, + customer, + order: order.toString(), + }) + // inserting same record twice with a different cause is a noop and + // we just ignore it. + .onConflict((oc) => oc.constraint('task_cid').doNothing()) + .returning('cause') + .executeTakeFirstOrThrow() + + return { cause: result.cause } + } catch (error) { + return new D1Error( + /** @type {import('../bindings').D1ErrorRaw} */ (error) + ) + } + } + + /** + * @param {API.SubscriptionQuery} query + */ + async find({ provider, customer, order }) { + let query = this.d1.selectFrom(this.tableName).selectAll() + if (customer) { + query = query.where('subscriptions.customer', '=', customer) + } + + if (provider) { + query = query.where('subscriptions.provider', '=', provider) + } + + if (order) { + query = query.where('subscriptions.order', '=', order) + } + + return await query.execute() + } +} diff --git a/packages/access-api/src/models/validations.js b/packages/access-api/src/models/validations.js index 5f36865cb..0567e5f7b 100644 --- a/packages/access-api/src/models/validations.js +++ b/packages/access-api/src/models/validations.js @@ -1,7 +1,9 @@ import { stringToDelegation } from '@web3-storage/access/encoding' +import * as API from '../types/index.js' /** * Validations + * @implements {API.ValidationStore} */ export class Validations { /** @@ -13,8 +15,9 @@ export class Validations { } /** - * @template {import('@ucanto/interface').Capabilities} [T=import('@ucanto/interface').Capabilities] + * @template {import('@ucanto/interface').Capabilities} T * @param {import('@web3-storage/access/src/types').EncodedDelegation} ucan + * @returns {Promise>} */ async put(ucan) { const delegation = diff --git a/packages/access-api/src/routes/validate-email.js b/packages/access-api/src/routes/validate-email.js index ea3d8b199..3b6211d72 100644 --- a/packages/access-api/src/routes/validate-email.js +++ b/packages/access-api/src/routes/validate-email.js @@ -145,7 +145,7 @@ async function authorize(req, env) { const request = stringToDelegation(req.query.ucan) const confirmation = await validator.access(request, { - capability: Access.confirm, + capability: Access.authorize, principal: Verifier, authority: env.signer, }) diff --git a/packages/access-api/src/service/access-authorize.js b/packages/access-api/src/service/access-authorize.js index 8d9a3ce56..c25561a33 100644 --- a/packages/access-api/src/service/access-authorize.js +++ b/packages/access-api/src/service/access-authorize.js @@ -1,59 +1,106 @@ import * as Server from '@ucanto/server' import * as Access from '@web3-storage/capabilities/access' -import * as Mailto from '../utils/did-mailto.js' -import * as DID from '@ipld/dag-ucan/did' -import { delegationToString } from '@web3-storage/access/encoding' +import * as Capabilities from '@web3-storage/capabilities/types' +import { delegationsToString } from '@web3-storage/access/encoding' +import { Absentee, Verifier } from '@ucanto/principal' +import { delegate } from '@ucanto/core' +import * as API from '../types/index.js' /** - * @param {import('../bindings').RouteContext} ctx + * @typedef {object} Context + * @property {object} models + * @property {API.ConsumerStore} models.consumers + * @property {API.SubscriptionStore} models.subscriptions + * @property {API.DelegationStore} models.delegations + * @property {API.ValidationStore} models.validations + * @property {Server.Signer>} signer + * @property {URL} url + * @property {import('../bindings').Email} email */ -export function accessAuthorizeProvider(ctx) { - return Server.provide(Access.authorize, async ({ capability }) => { - /** - * We issue `access/confirm` invocation which will - * get embedded in the URL that we send to the user. When user clicks the - * link we'll get this delegation back in the `/validate-email` endpoint - * which will allow us to verify that it was the user who clicked the link - * and not some attacker impersonating the user. We will know that because - * the `with` field is our service DID and only private key holder is able - * to issue such delegation. - * - * We limit lifetime of this UCAN to 15 minutes to reduce the attack - * surface where an attacker could attempt concurrent authorization - * request in attempt confuse a user into clicking the wrong link. - */ - const confirmation = await Access.confirm - .invoke({ - issuer: ctx.signer, - audience: DID.parse(capability.nb.iss), - // Because with is set to our DID no other actor will be able to issue - // this delegation without our private key. - with: ctx.signer.did(), - lifetimeInSeconds: 60 * 15, // 15 minutes - // We link to the authorization request so that this attestation can - // not be used to authorize a different request. - nb: { - // we copy request details and set the `aud` field to the agent DID - // that requested the authorization. - ...capability.nb, - aud: capability.with, - }, - }) - .delegate() - - await ctx.models.accounts.create(capability.nb.iss) - - // Encode authorization request and our attestation as string so that it - // can be passed as a query parameter in the URL. - const encoded = delegationToString(confirmation) - - const url = `${ctx.url.protocol}//${ctx.url.host}/validate-email?ucan=${encoded}&mode=authorize` - - await ctx.email.sendValidation({ - to: Mailto.toEmail(capability.nb.iss), - url, - }) - - return {} + +/** + * @param {object} input + * @param {Capabilities.AccessAuthorize} input.capability + * @param {Context} context + * @returns {Promise>} + */ +export const authorize = async ({ capability }, context) => { + const { account, agent, capabilities } = decodeAuthorization(capability) + const proofs = await context.models.delegations.find({ + audience: account.did(), + }) + + // create a delegation on behalf of the account with an absent signature. + const delegation = await delegate({ + issuer: account, + audience: agent, + capabilities, + expiration: Infinity, + // We include all the delegations to the account so that the agent will + // have delegation chains to all the delegated resources. + // We should actually filter out only delegations that support delegated + // capabilities, but for now we just include all of them since we only + // implement sudo access anyway. + proofs, + }) + + const attestation = await Access.session.delegate({ + issuer: context.signer, + audience: agent, + with: context.signer.did(), + nb: { proof: delegation.cid }, + expiration: Infinity, }) + + // Store the delegations so that they can be pulled with access/claim + // The fact that we're storing proofs chains that we pulled from the + // database is not great, but it's a tradeoff we're making for now. + await context.models.delegations.putMany(delegation, attestation) + + const authorization = delegationsToString([delegation, attestation]) + // Save delegations for the validation process + await context.models.validations.putSession(authorization, agent.did()) + + return {} } + +/** + * @param {Capabilities.AccessAuthorize} capability + */ + +export const decodeAuthorization = (capability) => { + const { from, to, access } = capability.nb + const account = Absentee.from({ + id: /** @type {Server.API.DID<'mailto'>} */ (from), + }) + const agent = Verifier.parse(to) + const capabilities = decodeAccess(access) + + return { account, agent, capabilities } +} + +/** + * + * @param {Capabilities.AccessAuthorize['nb']['access']} access + */ +export const decodeAccess = (access) => { + const capabilities = [] + for (const [uri, abilities] of Object.entries(access)) { + for (const [ability, caveats] of Object.entries(abilities)) { + const options = caveats.length === 0 ? [{}] : caveats + for (const nb of options) { + capabilities.push( + /** @type {Server.API.Capability} */ ({ with: uri, can: ability, nb }) + ) + } + } + } + + return /** @type {Server.API.Capabilities} */ (capabilities) +} + +/** + * @param {import('../bindings').RouteContext} context + */ +export const provide = (context) => + Server.provide(Access.authorize, (input) => authorize(input, context)) diff --git a/packages/access-api/src/service/access-claim.js b/packages/access-api/src/service/access-claim.js index 350286377..7dec9a06f 100644 --- a/packages/access-api/src/service/access-claim.js +++ b/packages/access-api/src/service/access-claim.js @@ -17,7 +17,7 @@ import { collect } from 'streaming-iterables' /** * @param {object} ctx - * @param {import('../types/delegations').DelegationsStorage} ctx.delegations + * @param {import('../types/delegations').DelegationStore} ctx.delegations * @param {Pick} ctx.config */ export function accessClaimProvider(ctx) { @@ -34,7 +34,7 @@ export function accessClaimProvider(ctx) { /** * @param {object} options - * @param {import('../types/delegations').DelegationsStorage} options.delegations + * @param {import('../types/delegations').DelegationStore} options.delegations * @returns {AccessClaimHandler} */ export function createAccessClaimHandler({ delegations }) { diff --git a/packages/access-api/src/service/access-delegate.js b/packages/access-api/src/service/access-delegate.js index 45ddcf328..eeae0deed 100644 --- a/packages/access-api/src/service/access-delegate.js +++ b/packages/access-api/src/service/access-delegate.js @@ -21,7 +21,7 @@ import { createDelegationsStorage } from './delegations.js' /** * @param {object} ctx - * @param {import('../types/delegations').DelegationsStorage} ctx.delegations + * @param {import('../types/delegations').DelegationStore} ctx.delegations * @param {HasStorageProvider} ctx.hasStorageProvider */ export function accessDelegateProvider(ctx) { @@ -49,7 +49,7 @@ export function accessDelegateProvider(ctx) { /** * @param {object} options - * @param {import('../types/delegations').DelegationsStorage} [options.delegations] + * @param {import('../types/delegations').DelegationStore} [options.delegations] * @param {HasStorageProvider} [options.hasStorageProvider] * @param {boolean} [options.allowServiceWithoutStorageProvider] - whether to allow service if the capability resource does not have a storage provider * @returns {AccessDelegateHandler} diff --git a/packages/access-api/src/service/access-request.js b/packages/access-api/src/service/access-request.js new file mode 100644 index 000000000..8af335f5b --- /dev/null +++ b/packages/access-api/src/service/access-request.js @@ -0,0 +1,169 @@ +/* eslint-disable unicorn/new-for-builtins, max-depth */ +import * as Server from '@ucanto/server' +import { ed25519, Absentee } from '@ucanto/principal' +import { sha256 } from 'multiformats/hashes/sha2' +import { Access } from '@web3-storage/capabilities' +import * as Capabilities from '@web3-storage/capabilities/types' +import * as Mailto from '../utils/did-mailto.js' +import { delegationToString } from '@web3-storage/access/encoding' +import * as Customer from './customer.js' +import * as Pin from '../utils/pin.js' + +/** + * @typedef {object} Context + * @property {object} models + * @property {import('../types/consumers').ConsumerStore} models.consumers + * @property {import('../types/subscriptions').SubscriptionStore} models.subscriptions + * @property {import('../types/delegations').DelegationStore} models.delegations + * @property {Server.Signer>} signer + * @property {URL} url + * @property {import('../bindings').Email} email + * + * @param {object} input + * @param {Capabilities.AccessRequest} input.capability + * @param {{ cid: Capabilities.Link }} input.invocation + * @param {Context} context + * @returns {Promise>} + */ +export const request = async ({ capability, invocation }, context) => { + const provider = context.signer + const account = Absentee.from({ id: capability.nb.from }) + const customer = await Customer.createCustomer({ + provider, + customer: account, + }) + + // We try to add the customer subscription to the provider, if one already + // exists this will be a noop. If it did not exist one will be created and + // `consumer/*` capability will be delegated to the account. + const result = await Customer.add(customer, context) + // This should never happen because adding same subscription twice is a noop + // yet we check and propagate error just in case. + if (result.error) { + return result + } + + // We will build a verification URL that we will send to the user in order + // to request an authorization. + const url = new URL(context.url) + url.pathname = '/validate-email' + + // We want to limit the window in witch the user can approve the request + // to limit the risk of unintended approval. + const lifetimeInSeconds = 60 * 15 // 15 minutes + + // we generate a random 6 digit pin and a delegate keypair from the account + // did and the pin. This will allow a an account holder to generate a delegate + // keypair from the pin and allow them to approve the request with it. + const pin = Pin.generate() + + // Delegate `access/authorize` to the session principal so it can be used + // to approve the requested access. In the future we will only pass the + // delegation along with the private key so that the user can decide which + // capabilities to grant. For now we will also create an invocation so that + // user can approve with a simple click. + const authorization = await authorize({ + service: provider, + account, + agent: capability.with, + access: capability.nb.access, + pin, + lifetimeInSeconds, + }) + // encode the delegation as a query parameter and add it to the URL + url.searchParams.set('ucan', await delegationToString(authorization)) + url.searchParams.set('pin', pin.join('')) + url.searchParams.set('mode', 'authorize') + + await context.email.sendValidation({ + to: Mailto.toEmail(account.did()), + url: url.toString(), + }) + + return { ran: invocation.cid } +} + +/** + * @param {Context} context + */ +export const provide = (context) => + Server.provide(Access.request, (input) => request(input, context)) + +/** + * @param {object} input + * @param {number[]} input.pin + * @param {Server.Signer>} input.service + * @param {Server.UCAN.Signer>} input.account + * @param {Server.API.DID} input.agent + * @param {Capabilities.AccessRequest['nb']['access']} input.access + * @param {number} input.lifetimeInSeconds + */ +export const authorize = async ({ + service, + account, + agent, + access, + pin, + lifetimeInSeconds, +}) => { + const delegate = await createSession({ + pin, + account: account.did(), + }) + + // We delegate `access/authorize` capability to this delegate so that it can + // approve requested access by invoking it. + const authorization = await Access.authorize.delegate({ + issuer: account, + audience: delegate, + with: account.did(), + lifetimeInSeconds, + }) + + // We also issue an for the above authorization to give a delegate a proof + // needed to invoke above delegation. + const attestation = await Access.session + .invoke({ + issuer: service, + audience: delegate, + with: service.did(), + nb: { proof: authorization.cid }, + lifetimeInSeconds, + }) + .delegate() + + // In the future we could store above delegations using `access/delegate` and + // send the delegate keypair to the user email which would allow them to + // decide exactly what they want to grant presumably through the UI we will + // build. For now however we will just create an invocation from the delegate + // and send that to the user email allowing user to approve requested access + // with a simple click. + return await Access.authorize + .invoke({ + issuer: delegate, + audience: service, + with: account.did(), + nb: { + agent, + access, + }, + lifetimeInSeconds, + proofs: [authorization, attestation], + }) + .delegate() +} + +/** + * We create a temporary delegate to represent an account during an + * authorization session. We derive a keypair from the account and a random + * 6 digit pin. We then delegate `access/authorize` capability to this delegate + * + * @param {object} input + * @param {number[]} input.pin + * @param {string} input.account + */ +const createSession = async ({ account, pin }) => { + const seed = new TextEncoder().encode(JSON.stringify({ account, pin })) + const secret = await sha256.digest(seed) + return await ed25519.derive(secret.digest) +} diff --git a/packages/access-api/src/service/customer.js b/packages/access-api/src/service/customer.js new file mode 100644 index 000000000..9222943b7 --- /dev/null +++ b/packages/access-api/src/service/customer.js @@ -0,0 +1,132 @@ +import * as Server from '@ucanto/server' +import { Customer, Provision } from '@web3-storage/capabilities' +import * as Capabilities from '@web3-storage/capabilities/types' + +import { codec as CBOR } from '@ucanto/transport/cbor' +import * as Mailto from '../utils/did-mailto.js' +import { claim, Failure } from '@ucanto/validator' +import { createProvision } from './provision.js' +import { Verifier } from '@ucanto/principal/ed25519' + +/** + * @typedef {object} Context + * @property {object} models + * @property {import('../types/subscriptions').SubscriptionStore} models.subscriptions + * @property {import('../types/delegations').DelegationStore} models.delegations + * @property {Server.Signer>} signer + */ + +/** + * @param {object} input + * @param {{ cid: Capabilities.Link, proofs: Server.Proof[] }} input.invocation + * @param {Context} context + * @returns {Promise>} + */ +export const add = async ({ invocation }, context) => { + const provision = await claim(Provision.provision, invocation.proofs, { + // ⚠️ This will not going to work when provider did is different from + // service did as we'll need a way to resolve provider key. + authority: context.signer, + principal: Verifier, + }) + + // we ensure that invocation includes a delegation to the + if (provision.error) { + return new Failure( + `Expected 'nb.access' to delegate 'consumer/*' which is not the case: ${provision}` + ) + } + + // we only accept delegation without expiration + if (provision.delegation.expiration === Infinity) { + return new Failure( + `Expect 'nb.access' to delegate non-expiring 'consumer/*' capability to the customer` + ) + } + + // Attempt to to store a new subscription + const subscription = await context.models.subscriptions.add({ + cause: invocation.cid, + provision: provision.delegation.cid, + provider: provision.capability.with, + customer: provision.capability.nb.customer, + order: provision.capability.nb.order, + }) + + if (subscription.error) { + return new Failure(`Failed to create subscription: ${subscription}`) + } + + // We detect whether insert occurred or if this operation was a noop + // by checking if `cause` matches invocation cid. If insert took place + // we'll save a delegation into delegation store. + if (subscription.cause.toString() === invocation.cid.toString()) { + context.models.delegations.putMany(provision.delegation) + } + + return { cause: subscription.cause } +} + +/** + * @param {object} input + * @param {Capabilities.CustomerList} input.capability + * @param {Context} context + * @returns {Promise>} + */ +export const list = async ({ capability }, context) => { + const subscriptions = await context.models.subscriptions.find({ + provider: capability.with, + customer: capability.nb.customer, + order: capability.nb.order, + }) + + return { results: subscriptions } +} + +/** + * @param {import('../bindings').RouteContext} context + */ +export const provide = (context) => ({ + add: Server.provide(Customer.add, (input) => add(input, context)), + list: Server.provide(Customer.list, (input) => list(input, context)), +}) + +/** + * @param {object} input + * @param {Server.Signer>} input.provider + * @param {Server.Principal>} input.customer + */ +export const createCustomer = async ({ provider, customer }) => { + const order = await createOrder({ customer }) + // We want to give account full access to the provider subscription so we + // delegate `provision/*` capability to it. + const provision = await createProvision({ customer, provider, order }) + + const invocation = await Customer.add + .invoke({ + issuer: provider, + audience: provider, + with: provider.did(), + nb: { + provision: provision.cid, + }, + proofs: [provision], + }) + .delegate() + + const [capability] = invocation.capabilities + + return { invocation, capability, order, provision } +} + +/** + * Creates an order for the given customer. + * + * @param {object} input + * @param {Server.Principal>} input.customer + */ + +export const createOrder = async ({ customer }) => { + const { cid } = await CBOR.write({ mailto: Mailto.toEmail(customer.did()) }) + return cid +} diff --git a/packages/access-api/src/service/delegations.js b/packages/access-api/src/service/delegations.js index 0e99544ba..6ad940898 100644 --- a/packages/access-api/src/service/delegations.js +++ b/packages/access-api/src/service/delegations.js @@ -11,20 +11,20 @@ import * as Ucanto from '@ucanto/interface' * DelegationsStorage that stores in-memory. * * @param {Pick, 'length' | 'push' | SymbolIterator>} storage - * @returns {import("../types/delegations").DelegationsStorage} + * @returns {import("../types/delegations").DelegationStore} */ export function createDelegationsStorage(storage = []) { - /** @type {import("../types/delegations").DelegationsStorage[typeof Symbol.asyncIterator]} */ + /** @type {import("../types/delegations").DelegationStore[typeof Symbol.asyncIterator]} */ async function* asyncIterator() { for (const delegation of storage) { yield delegation } } - /** @type {import("../types/delegations").DelegationsStorage['count']} */ + /** @type {import("../types/delegations").DelegationStore['count']} */ async function count() { return BigInt(storage.length) } - /** @type {import("../types/delegations").DelegationsStorage['find']} */ + /** @type {import("../types/delegations").DelegationStore['find']} */ async function* find(query) { for (const d of storage) { if (d.audience.did() === query.audience) { @@ -32,11 +32,11 @@ export function createDelegationsStorage(storage = []) { } } } - /** @type {import("../types/delegations").DelegationsStorage['putMany']} */ + /** @type {import("../types/delegations").DelegationStore['putMany']} */ async function putMany(...args) { return storage.push(...args) } - /** @type {import('../types/delegations').DelegationsStorage} */ + /** @type {import('../types/delegations').DelegationStore} */ const delegations = { [Symbol.asyncIterator]: asyncIterator, count, diff --git a/packages/access-api/src/service/index.js b/packages/access-api/src/service/index.js index 4ecb848cc..d9d84a726 100644 --- a/packages/access-api/src/service/index.js +++ b/packages/access-api/src/service/index.js @@ -13,12 +13,15 @@ import { import { voucherClaimProvider } from './voucher-claim.js' import { voucherRedeemProvider } from './voucher-redeem.js' import * as uploadApi from './upload-api-proxy.js' -import { accessAuthorizeProvider } from './access-authorize.js' +import * as AccessAuthorize from './access-authorize.js' +import * as AccessRequest from './access-request.js' import { accessDelegateProvider } from './access-delegate.js' import { accessClaimProvider } from './access-claim.js' -import { providerAddProvider } from './provider-add.js' +import * as ProviderAdd from './provider-add.js' import { Spaces } from '../models/spaces.js' -import { handleAccessConfirm } from './access-confirm.js' +import * as Subscriptions from './subscription.js' +import * as Customer from './customer.js' +import * as Provision from './provision.js' /** * @param {import('../bindings').RouteContext} ctx @@ -35,8 +38,13 @@ export function service(ctx) { store: uploadApi.createStoreProxy(ctx), upload: uploadApi.createUploadProxy(ctx), + provision: Provision.provide(ctx), + subscription: Subscriptions.provide(ctx), + customer: Customer.provide(ctx), + access: { - authorize: accessAuthorizeProvider(ctx), + authorize: AccessAuthorize.provide(ctx), + request: AccessRequest.provide(ctx), claim: (...args) => { // disable until hardened in test/staging if (ctx.config.ENV === 'production') { @@ -75,13 +83,7 @@ export function service(ctx) { }, provider: { - add: (...args) => { - // disable until hardened in test/staging - if (ctx.config.ENV === 'production') { - throw new Error(`provider/add invocation handling is not enabled`) - } - return providerAddProvider(ctx)(...args) - }, + add: ProviderAdd.provide(ctx), }, voucher: { @@ -254,7 +256,7 @@ export function service(ctx) { * @template {Ucanto.DID} Service * @param {Ucanto.DID<'key'>} space * @param {Spaces} spaces - * @param {import('../types/provisions.js').ProvisionsStorage} provisions + * @param {import('../types/provisions.js').ProvisionStore} provisions * @returns {Promise} */ async function spaceHasStorageProvider(space, spaces, provisions) { @@ -277,7 +279,7 @@ async function spaceHasStorageProviderFromVoucherRedeem(space, spaces) { /** * @template {Ucanto.DID} Service * @param {Ucanto.DID<'key'>} space - * @param {import('../types/provisions.js').ProvisionsStorage} provisions + * @param {import('../types/provisions.js').ProvisionStore} provisions * @returns {Promise} */ async function spaceHasStorageProviderFromProviderAdd(space, provisions) { diff --git a/packages/access-api/src/service/provider-add.js b/packages/access-api/src/service/provider-add.js index 7253be3d9..8f2f728df 100644 --- a/packages/access-api/src/service/provider-add.js +++ b/packages/access-api/src/service/provider-add.js @@ -1,64 +1,81 @@ -import * as Ucanto from '@ucanto/interface' import * as Server from '@ucanto/server' -import { Provider } from '@web3-storage/capabilities' -import * as validator from '@ucanto/validator' +import { Provider, Schema } from '@web3-storage/capabilities' +import * as API from '../types/index.js' +import * as Capabilities from '@web3-storage/capabilities/types' +import * as Customer from './customer.js' +import { Absentee } from '@ucanto/principal' /** - * @typedef {import('@web3-storage/capabilities/types').ProviderAdd} ProviderAdd - * @typedef {import('@web3-storage/capabilities/types').ProviderAddSuccess} ProviderAddSuccess - * @typedef {import('@web3-storage/capabilities/types').ProviderAddFailure} ProviderAddFailure + * @typedef {object} Context + * @property {object} models + * @property {API.ConsumerStore} models.consumers + * @property {API.SubscriptionStore} models.subscriptions + * @property {API.DelegationStore} models.delegations + * @property {object} config + * @property {string} config.ENV + * @property {Server.Signer>} signer + * + * @param {object} input + * @param {Capabilities.ProviderAdd} input.capability + * @param {{ cid: Server.API.Link }} input.invocation + * @param {Context} context + * @returns {Promise>} */ +export const add = async ({ capability, invocation }, context) => { + // disable until hardened in test/staging + if (context.config.ENV === 'production') { + throw new Error(`provider/add invocation handling is not enabled`) + } -/** - * @callback ProviderAddHandler - * @param {Ucanto.Invocation} invocation - * @returns {Promise>} - */ + const { consumer, provider } = capability.nb + const account = parseAccount(capability.with) + if (account.error) { + return new Server.Failure(`Must be invoked with an account: ${account}`) + } + + if (provider !== context.signer.did()) { + return new Server.Failure( + `Expected provider to be '${context.signer.did()}' but got '${provider}' instead` + ) + } + + // Create a subscription for this account with a provider. If one already + // exists this will be a noop. If it did not exist one will be created and + // `consumer/*` capability will be delegated to the account. + const customer = await Customer.createCustomer({ + provider: context.signer, + customer: account, + }) + const result = await Customer.add(customer, context) + // This should never happen because adding same subscription twice is a noop + // yet we check and propagate error just in case. + if (result.error) { + return result + } + + // Then we add a consumer to subscription for the account + return await context.models.consumers.add({ + cause: invocation.cid, + provider: context.signer.did(), + consumer, + order: customer.order, + }) +} /** - * @template {Ucanto.DID} ServiceId - * @param {object} options - * @param {import('../types/provisions').ProvisionsStorage} options.provisions - * @returns {ProviderAddHandler} + * @param {string} input + * @returns {Server.Result>, Server.API.Failure>} */ -export function createProviderAddHandler(options) { - /** @type {ProviderAddHandler} */ - return async (invocation) => { - const [providerAddCap] = invocation.capabilities - const { - nb: { consumer, provider }, - with: accountDID, - } = providerAddCap - if (!validator.DID.match({ method: 'mailto' }).is(accountDID)) { - return { - error: true, - name: 'Unauthorized', - message: 'Issuer must be a mailto DID', - } - } - if (provider !== options.provisions.service) { - throw new Error(`Provider must be ${options.provisions.service}`) - } - await options.provisions.put({ - invocation, - space: consumer, - // eslint-disable-next-line object-shorthand - provider: /** @type {ServiceId} */ (provider), - account: accountDID, - }) - return {} +export const parseAccount = (input) => { + const result = Schema.Account.read(input) + if (result.error) { + return result } + return Absentee.from({ id: result }) } /** - * @param {object} ctx - * @param {Pick} ctx.models + * @param {Context} context */ -export function providerAddProvider(ctx) { - return Server.provide(Provider.add, async ({ invocation }) => { - const handler = createProviderAddHandler({ - provisions: ctx.models.provisions, - }) - return handler(/** @type {Ucanto.Invocation} */ (invocation)) - }) -} +export const provide = (context) => + Server.provide(Provider.add, async (input) => add(input, context)) diff --git a/packages/access-api/src/service/provision.js b/packages/access-api/src/service/provision.js new file mode 100644 index 000000000..c8d809aaa --- /dev/null +++ b/packages/access-api/src/service/provision.js @@ -0,0 +1,134 @@ +import * as Server from '@ucanto/server' +import * as Capabilities from '@web3-storage/capabilities/types' +import * as API from '../types/index.js' +import { Provision } from '@web3-storage/capabilities' + +/** + * @typedef {object} Context + * @property {object} models + * @property {API.ConsumerStore} models.consumers + * @property {Server.Signer>} signer + * @property {object} config + * @property {string} config.ENV + * + * @param {object} input + * @param {Capabilities.ProvisionAdd} input.capability + * @param {{ cid: Server.API.Link }} input.invocation + * @param {Context} context + * @returns {Promise>} + */ +export const add = async ({ capability, invocation }, context) => { + const { + with: provider, + nb: { consumer, order }, + } = capability + + // disable until hardened in test/staging + if (context.config.ENV === 'production') { + throw new Error(`provider/add invocation handling is not enabled`) + } + + // At the moment, we only support DID corresponding to our own DID. + if (provider !== context.signer.did()) { + return new Server.Failure( + `Expected provider to be '${context.signer.did()}' but got '${provider}' instead` + ) + } + + // Then we add a consumer to subscription for the account + return await context.models.consumers.add({ + cause: invocation.cid, + consumer, + provider, + order, + }) +} + +/** + * @param {object} input + * @param {Capabilities.ProvisionRemove} input.capability + * @param {{ cid: Server.API.Link }} input.invocation + * @param {Context} context + * @returns {Promise>} + */ +export const remove = async ({ capability, invocation }, context) => { + const { + with: provider, + nb: { consumer, order }, + } = capability + + // disable until hardened in test/staging + if (context.config.ENV === 'production') { + throw new Error(`provider/add invocation handling is not enabled`) + } + + // Then we add a consumer to subscription for the account + return await context.models.consumers.add({ + cause: invocation.cid, + consumer, + provider, + order, + }) +} + +/** + * @param {object} input + * @param {Capabilities.ProvisionList} input.capability + * @param {Context} context + * @returns {Promise>} + */ +export const list = async ({ capability }, context) => { + const { + with: provider, + nb: { order }, + } = capability + + // disable until hardened in test/staging + if (context.config.ENV === 'production') { + throw new Error(`provider/add invocation handling is not enabled`) + } + + // Then we add a consumer to subscription for the account + return await context.models.consumers.find({ + provider, + order, + }) +} + +/** + * Create an authorization for the given customer that allows them to add/remove + * consumers to the subscription. + * + * + * @param {object} input + * @param {Server.API.Link} input.order + * @param {Server.Signer>} input.provider + * @param {Server.Principal>} input.customer + */ +export const createProvision = async ({ provider, customer, order }) => { + // We want to give account full access to the provider subscription so we + // delegate `consumer/*` capability to it. + return await Provision.provision + .invoke({ + issuer: provider, + expiration: Infinity, + audience: customer, + with: provider.did(), + nb: { + customer: customer.did(), + order, + }, + }) + .delegate() +} + +/** + * @param {Context} context + */ +export const provide = (context) => ({ + add: Server.provide(Provision.add, async (input) => add(input, context)), + remove: Server.provide(Provision.remove, async (input) => + remove(input, context) + ), + list: Server.provide(Provision.list, async (input) => list(input, context)), +}) diff --git a/packages/access-api/src/service/subscription.js b/packages/access-api/src/service/subscription.js new file mode 100644 index 000000000..407f970b5 --- /dev/null +++ b/packages/access-api/src/service/subscription.js @@ -0,0 +1,35 @@ +import * as Server from '@ucanto/server' +import { Subscription } from '@web3-storage/capabilities' +import * as Capabilities from '@web3-storage/capabilities/types' + +/** + * @typedef {object} Context + * @property {object} models + * @property {import('../types/subscriptions').SubscriptionStore} models.subscriptions + * @property {Server.Signer>} signer + * @property {URL} url + * @property {import('../bindings').Email} email + */ + +/** + * @param {object} input + * @param {Capabilities.SubscriptionList} input.capability + * @param {Context} context + * @returns {Promise>} + */ +export const list = async ({ capability }, context) => { + const subscriptions = await context.models.subscriptions.find({ + customer: capability.with, + provider: capability.nb.provider, + order: capability.nb.order, + }) + + return { results: subscriptions } +} + +/** + * @param {import('../bindings').RouteContext} context + */ +export const provide = (context) => ({ + list: Server.provide(Subscription.list, (input) => list(input, context)), +}) diff --git a/packages/access-api/src/types/accounts.ts b/packages/access-api/src/types/accounts.ts new file mode 100644 index 000000000..278d5620d --- /dev/null +++ b/packages/access-api/src/types/accounts.ts @@ -0,0 +1,12 @@ +import { DID } from '@ucanto/interface' + +export interface AccountRecord { + did: DID + inserted_at: Date + updated_at: Date +} + +export interface AccountStore { + create: (did: DID) => Promise<{ data: Array<{ did: DID }> }> + get: (did: DID) => Promise +} diff --git a/packages/access-api/src/types/consumers.ts b/packages/access-api/src/types/consumers.ts new file mode 100644 index 000000000..b1ab8e418 --- /dev/null +++ b/packages/access-api/src/types/consumers.ts @@ -0,0 +1,57 @@ +import type { Generated, Text, Row, Table } from './database.js' +import type { DID, Link, Result, Failure } from '@ucanto/interface' +export * from './database.js' + +export interface Consumer { + /** + * Provider that provides service to the customer. + */ + provider: DID<'web'> + + /** + * Identifier generated by the provider to identify this subscription. + */ + order: Text + + /** + * DID of the consumer of the consumer space. + */ + consumer: DID<'key'> +} + +export interface ConsumerID { + /** + * CID of the invocation that created this subscription + */ + cause: Text +} + +export interface ConsumerAdd extends Consumer, ConsumerID {} + +export interface ConsumerRemove extends Consumer {} + +export interface ConsumerRecord extends ConsumerAdd, Row { + /** + * Unique identifier for this subscription. + */ + subscription: Generated +} + +export type ConsumerTable = Table + +export interface ConsumerStore { + add: (consumer: ConsumerAdd) => Promise> + remove: ( + consumer: ConsumerRemove + ) => Promise> + + find: (query: ConsumerQuery) => Promise +} + +export interface ConsumerQuery { + provider?: DID<'web'> + customer?: DID<'mailto'> + consumer?: DID<'key'> + + order?: Link +} diff --git a/packages/access-api/src/types/database.ts b/packages/access-api/src/types/database.ts index bdaa04929..bf5ec5dc2 100644 --- a/packages/access-api/src/types/database.ts +++ b/packages/access-api/src/types/database.ts @@ -1,9 +1,36 @@ -import { Kysely } from 'kysely' +import * as Kysely from 'kysely' -export type Database = Kysely & { +export type Database = Kysely.Kysely & { /** - * whether or not this Databse supports Kysely stream() asyncIterator + * whether or not this Database supports Kysely stream() asyncIterator * (kysely-d1 dialect does not) */ - canStream: boolean + canStream?: boolean +} + +declare const column: unique symbol + +export type Column< + SelectType, + InsertType = SelectType, + UpdateType = InsertType +> = SelectType & { + [column]?: Kysely.ColumnType +} + +export type Text = Column + +export type Timestamp = Column + +export type Generated = Column + +export interface Row { + updated_at: Timestamp + inserted_at: Timestamp +} + +export type Table = { + [Key in keyof Model]: Model[Key] extends Column + ? Kysely.ColumnType + : Model[Key] } diff --git a/packages/access-api/src/types/delegations.ts b/packages/access-api/src/types/delegations.ts index b72b439c4..d368dd408 100644 --- a/packages/access-api/src/types/delegations.ts +++ b/packages/access-api/src/types/delegations.ts @@ -3,9 +3,9 @@ import * as Ucanto from '@ucanto/interface' interface ByAudience { audience: Ucanto.DID<'key' | 'mailto'> } -export type Query = ByAudience +export type DelegationQuery = ByAudience -export interface DelegationsStorage< +export interface DelegationStore< Cap extends Ucanto.Capability = Ucanto.Capability > { /** @@ -32,5 +32,7 @@ export interface DelegationsStorage< /** * find all items that match the query */ - find: (query: Query) => AsyncIterable>> + find: ( + query: DelegationQuery + ) => Promise>>> } diff --git a/packages/access-api/src/types/index.js b/packages/access-api/src/types/index.js new file mode 100644 index 000000000..336ce12bb --- /dev/null +++ b/packages/access-api/src/types/index.js @@ -0,0 +1 @@ +export {} diff --git a/packages/access-api/src/types/index.ts b/packages/access-api/src/types/index.ts new file mode 100644 index 000000000..8d8c713e3 --- /dev/null +++ b/packages/access-api/src/types/index.ts @@ -0,0 +1,7 @@ +export * from './consumers.js' +export * from './database.js' +export * from './delegations.js' +export * from './provisions.js' +export * from './subscriptions.js' +export * from './validations.js' +export * from './accounts.js' diff --git a/packages/access-api/src/types/provisions.ts b/packages/access-api/src/types/provisions.ts index f87620d4b..7ffc8d64d 100644 --- a/packages/access-api/src/types/provisions.ts +++ b/packages/access-api/src/types/provisions.ts @@ -16,7 +16,7 @@ export interface Provision> { /** * stores instances of a storage provider being consumed by a consumer */ -export interface ProvisionsStorage> { +export interface ProvisionStore> { service: ServiceDID hasStorageProvider: (consumer: Ucanto.DID<'key'>) => Promise /** diff --git a/packages/access-api/src/types/subscriptions.ts b/packages/access-api/src/types/subscriptions.ts new file mode 100644 index 000000000..3db1984d1 --- /dev/null +++ b/packages/access-api/src/types/subscriptions.ts @@ -0,0 +1,25 @@ +import type { DID, Link, Result, Failure } from '@ucanto/interface' +import type { Text, Row } from './database.js' +import type * as Capabilities from '@web3-storage/capabilities/types' + +export interface SubscriptionID { + cause: Text +} + +export interface Subscription extends Capabilities.Subscription { + cause: Text + order: Text +} + +export interface SubscriptionRecord extends Subscription, Row {} + +export interface SubscriptionStore { + add: (subscription: Subscription) => Promise> + find: (query: SubscriptionQuery) => Promise +} + +export interface SubscriptionQuery { + customer?: DID<'mailto'> + provider?: DID<'web'> + order?: Link +} diff --git a/packages/access-api/src/types/validations.ts b/packages/access-api/src/types/validations.ts new file mode 100644 index 000000000..1e10a9f5d --- /dev/null +++ b/packages/access-api/src/types/validations.ts @@ -0,0 +1,16 @@ +import type { Capabilities, Delegation, DID } from '@ucanto/interface' +import type { EncodedDelegation } from '@web3-storage/access/src/types' + +export interface ValidationStore { + put: ( + ucan: EncodedDelegation + ) => Promise> + putSession: ( + ucan: EncodedDelegation, + agent: DID, + ttl?: number + ) => Promise + get: (did: string) => Promise> + + delete: (did: string) => Promise +} diff --git a/packages/access-api/src/utils/did-mailto.js b/packages/access-api/src/utils/did-mailto.js index 52db79170..7dc0c849a 100644 --- a/packages/access-api/src/utils/did-mailto.js +++ b/packages/access-api/src/utils/did-mailto.js @@ -1,7 +1,5 @@ /** - * - * @param {`did:${string}:${string}`} did - * @returns + * @param {`did:mailto:${string}`} did */ export function toEmail(did) { const parts = did.split(':') diff --git a/packages/access-api/src/utils/pin.js b/packages/access-api/src/utils/pin.js new file mode 100644 index 000000000..3534294aa --- /dev/null +++ b/packages/access-api/src/utils/pin.js @@ -0,0 +1,97 @@ +/* eslint-disable unicorn/no-null */ +/* eslint-disable no-nested-ternary */ + +/** + * Generates a random pin code of the given length (at most 9 digits) that can + * be mapped to a lock 3x3 grid lock pattern like this: + * + * ``` + * 0 1 2 + * 3 4 5 + * 6 7 8 + * ``` + * + * Generated pin codes will never contain the same digit twice and will never + * lead to a pattern where one of the dots is crossed without being part of the + * pin. + * + * @param {number} [length=6] + * @returns {number[]} + */ +export const generate = (length = 6) => { + const size = Math.min(length, 9) + // Allocate a buffer for 32 random bytes from which we will derive the pin + // Our pin will never be longer than 9 digits, but we may not be able to use + // every byte so we generate extra. + const bytes = new Uint8Array(32) + + // We will collect digits for the pin code here. + const pin = [] + // We will keep track of the digits we have already used here + // to avoid duplicates in the pin as it would lead to unclear + // lock patterns + const visited = new Set() + + // Loop until we have collected enough digits for the pin. + while (pin.length < size) { + // We fill the buffer with random bytes + crypto.getRandomValues(bytes) + // and iterate over them attempting to derive digits for the pin + // if digit is already in the pin or if it leads to an overlapping + // pattern we skip it and consider next byte. + for (const byte of bytes) { + // Map the random values to the numbers 0 to 8 + const digit = byte % 9 + // If this is the first digit we include it in the pin + const conflict = + pin.length === 0 + ? false + : // If we have already used this digit we skip it + // otherwise same dot will be used twice in the pattern + visited.has(digit) + ? true + : // If the digit is leading to a pattern with a line crossing + // dot that is not part of the pin we skip it + CONFLICT[pin[pin.length - 1]][digit] != null + + if (!conflict) { + visited.add(digit) + pin.push(digit) + } + + // If we already have enough digits we can stop here + if (pin.length === size) { + break + } + } + } + + return pin +} + +/** + * To ensue that the pin code does not lead to a lock pattern where one of the + * dots is crossed without being part of the pin we define conflicting digit + * sequences. For example, if we had a pin `0 2 5 8` it would produce a lock + * pattern where digit `1` is crossed by the line connecting `0` and `2` which + * is why `CONFLICT[0][2] === 1`. + * + * ``` + * ╋┅╋┅╋ + * 3 4 ╋ + * 6 7 ╋ + * ``` + * + * @type {Record>} + */ +const CONFLICT = { + 0: { 2: 1, 6: 3, 8: 4 }, + 1: { 7: 4 }, + 2: { 0: 1, 6: 4, 8: 5 }, + 3: { 5: 4 }, + 4: {}, + 5: { 3: 4 }, + 6: { 0: 3, 2: 4, 8: 7 }, + 7: { 1: 4 }, + 8: { 0: 4, 2: 5, 6: 7 }, +} diff --git a/packages/access-api/test/validate-email.test.js b/packages/access-api/test/validate-email.test.js index fd1eaeda7..eb03af430 100644 --- a/packages/access-api/test/validate-email.test.js +++ b/packages/access-api/test/validate-email.test.js @@ -16,7 +16,7 @@ describe('validate-email', () => { issuer: service, audience: agent, capabilities: [ - Access.confirm.create({ + Access.authorize.create({ with: service.did(), nb: { iss: accountDid, diff --git a/packages/access-client/src/types.ts b/packages/access-client/src/types.ts index 66a88a850..08692776c 100644 --- a/packages/access-client/src/types.ts +++ b/packages/access-client/src/types.ts @@ -32,6 +32,9 @@ import type { VoucherClaim, VoucherRedeem, Top, + AccessRequest, + AccessRequestSuccess, + AccessRequestFailure, AccessAuthorize, AccessAuthorizeSuccess, AccessDelegate, @@ -43,9 +46,21 @@ import type { ProviderAdd, ProviderAddSuccess, ProviderAddFailure, - AccessConfirm, - AccessConfirmSuccess, - AccessConfirmFailure, + CustomerList, + CustomerListSuccess, + CustomerListFailure, + CustomerAdd, + CustomerAddSuccess, + CustomerAddFailure, + SubscriptionList, + SubscriptionListSuccess, + SubscriptionListFailure, + ProvisionAdd, + ProvisionAddSuccess, + ProvisionAddFailure, + ProvisionList, + ProvisionListSuccess, + ProvisionListFailure, } from '@web3-storage/capabilities/types' import type { SetRequired } from 'type-fest' import { Driver } from './drivers/types.js' @@ -76,15 +91,15 @@ export type SpaceRecord = Selectable export type SpaceInfoResult = // w3up spaces registered via provider/add will have this | { - // space did - did: DID<'key'> - } + // space did + did: DID<'key'> + } // deprecated and may be removed if voucher/redeem is removed /** @deprecated */ | SpaceRecord export interface AccountTable { - did: URI<'did:'> + did: DID inserted_at: Generated updated_at: ColumnType } @@ -93,8 +108,8 @@ export type AccountRecord = Selectable export interface DelegationTable { cid: string bytes: Uint8Array - audience: URI<'did:'> - issuer: URI<'did:'> + audience: DID + issuer: DID expires_at: Date | null inserted_at: Generated updated_at: ColumnType @@ -112,23 +127,48 @@ export interface SpaceTableMetadata { */ export interface Service { access: { - authorize: ServiceMethod + request: ServiceMethod + authorize: ServiceMethod< + AccessAuthorize, + AccessAuthorizeSuccess, + AccessRequestFailure + > claim: ServiceMethod // eslint-disable-next-line @typescript-eslint/ban-types - confirm: ServiceMethod< - AccessConfirm, - AccessConfirmSuccess, - AccessConfirmFailure - > + // confirm: ServiceMethod< + // AccessConfirm, + // AccessConfirmSuccess, + // AccessConfirmFailure + // > delegate: ServiceMethod< AccessDelegate, AccessDelegateSuccess, AccessDelegateFailure > } + subscription: { + list: ServiceMethod< + SubscriptionList, + SubscriptionListSuccess, + SubscriptionListFailure + > + } + customer: { + add: ServiceMethod + list: ServiceMethod + } + provider: { add: ServiceMethod } + provision: { + add: ServiceMethod + list: ServiceMethod< + ProvisionList, + ProvisionListSuccess, + ProvisionListFailure + > + } voucher: { claim: ServiceMethod< VoucherClaim, @@ -320,21 +360,21 @@ export interface UCANBasicOptions { */ export type InferNb | undefined> = keyof C extends never - ? { - nb?: never - } - : { - /** - * Non-normative fields for the capability - * - * Check the capability definition for more details on the `nb` field. - * - * @see {@link https://github.com/ucan-wg/spec#241-nb-non-normative-fields Spec} - */ - nb: C - } + ? { + nb?: never + } + : { + /** + * Non-normative fields for the capability + * + * Check the capability definition for more details on the `nb` field. + * + * @see {@link https://github.com/ucan-wg/spec#241-nb-non-normative-fields Spec} + */ + nb: C + } -export interface ClientCodec extends RequestEncoder, ResponseDecoder {} +export interface ClientCodec extends RequestEncoder, ResponseDecoder { } export type EncodedDelegation = string & Phantom diff --git a/packages/capabilities/package.json b/packages/capabilities/package.json index ce4a5d5e5..736b58656 100644 --- a/packages/capabilities/package.json +++ b/packages/capabilities/package.json @@ -20,6 +20,7 @@ "test:node": "mocha 'test/**/*.test.js' -n experimental-vm-modules -n no-warnings", "test:browser": "playwright-test", "testw": "watch 'pnpm test:node' src test --interval 1", + "tq": "mocha --bail -n experimental-vm-modules -n no-warnings 'test/**/*.test.js'", "rc": "npm version prerelease --preid rc" }, "exports": { @@ -93,6 +94,7 @@ "unicorn/prefer-number-properties": "off", "unicorn/prefer-export-from": "off", "unicorn/no-array-reduce": "off", + "@typescript-eslint/no-empty-interface": "off", "jsdoc/no-undefined-types": [ "error", { diff --git a/packages/capabilities/src/access.js b/packages/capabilities/src/access.js index 61ab3e1cf..4730a8fe0 100644 --- a/packages/capabilities/src/access.js +++ b/packages/capabilities/src/access.js @@ -8,16 +8,20 @@ * * @module */ -import { capability, URI, DID, Link, Schema, Failure } from '@ucanto/validator' +import { + Ability, + Provider, + Account, + Agent, + capability, + DID, + Schema, + Failure, +} from './schema.js' import * as Types from '@ucanto/interface' import { equalWith, fail, equal } from './utils.js' export { top } from './top.js' -/** - * Account identifier. - */ -export const Account = DID.match({ method: 'mailto' }) - /** * Describes the capability requested. */ @@ -28,6 +32,12 @@ export const CapabilityRequest = Schema.struct({ can: Schema.string(), }) +export const Capability = Schema.struct({ + can: Schema.string(), + with: Schema.URI, + nb: Schema.unknown(), +}) + /** * Authorization request describing set of desired capabilities. */ @@ -49,25 +59,48 @@ export const AuthorizationRequest = Schema.struct({ */ export const access = capability({ can: 'access/*', - with: URI.match({ protocol: 'did:' }), + with: Schema.URI.match({ protocol: 'did:' }), +}) + +/** + * Describes set of abilities granted or requested. + */ +export const Allow = Schema.dictionary({ + key: Ability, + // we may allow additional details in the future but for now we only allow + // empty array. + value: Schema.never().array(), +}) + +/** + * Describes set of permissions granted or requested. It uses layout from + * [UCAN 0.10](https://github.com/ucan-wg/spec/pull/132) as opposed to 0.9 + * used currently to avoid breaking changes in the future. + */ +const Access = Schema.dictionary({ + key: Schema.URI, + value: Allow, }) /** * Capability can be invoked by an agent to request set of capabilities from * the account. */ -export const authorize = capability({ - can: 'access/authorize', +export const request = capability({ + can: 'access/request', with: DID.match({ method: 'key' }), /** * Authorization request describing set of desired capabilities */ - nb: AuthorizationRequest, + nb: Schema.struct({ + from: Account, + access: Access, + }), derives: (child, parent) => { return ( fail(equalWith(child, parent)) || - fail(equal(child.nb.iss, parent.nb.iss, 'iss')) || - fail(subsetCapabilities(child.nb.att, parent.nb.att)) || + fail(equal(child.nb.from, parent.nb.from, 'from')) || + fail(restrictAccess(child.nb.access, parent.nb.access)) || true ) }, @@ -79,25 +112,80 @@ export const authorize = capability({ * we don't have some rogue agent trying to impersonate user clicking the link * in order to get access to their account. */ -export const confirm = capability({ - can: 'access/confirm', - with: DID, +export const authorize = capability({ + can: 'access/authorize', + with: Account, nb: Schema.struct({ - iss: Account, - aud: Schema.did(), - att: CapabilityRequest.array(), + agent: Agent, + access: Access, }), derives: (claim, proof) => { return ( fail(equalWith(claim, proof)) || - fail(equal(claim.nb.iss, proof.nb.iss, 'iss')) || - fail(equal(claim.nb.aud, proof.nb.aud, 'aud')) || - fail(subsetCapabilities(claim.nb.att, proof.nb.att)) || + fail(equal(claim.nb.agent, claim.nb.agent, 'delegate')) || + fail(restrictAccess(claim.nb.access, proof.nb.access)) || true ) }, }) +/** + * + * @param {Record[]>>} granted + * @param {Record[]>>} approved + */ +const restrictAccess = (granted, approved) => { + const anyResource = approved['ucan:*'] + for (const [uri, value] of Object.entries(granted)) { + const resource = approved[uri] || anyResource + if (!resource) { + return new Failure(`Escalation resource '${uri}' has not been delegated`) + } + + for (const [can, caveats] of Object.entries(value)) { + const ability = resource[can] || resource['*'] + if (!ability) { + return new Failure( + `ability "${can}" has not been delegated for '${uri}'` + ) + } + + // if caveats are not specified then it means no caveats are imposed + // which is equivalent of `{}`. + const approved = ability.length > 0 ? ability : [{}] + const granted = caveats.length > 0 ? caveats : [{}] + + for (const need of granted) { + const satisfied = approved.some((allow) => isSubStruct(need, allow)) + if (!satisfied) { + return new Failure( + `Escalation ability ${can} on resource '${uri}' with caveats ${JSON.stringify( + need + )} violates imposed caveats ${JSON.stringify(ability)}` + ) + } + } + } + } + + return true +} + +/** + * @template {Record} T + * @template {Record} U + * @param {T} a + * @param {U} b + */ +const isSubStruct = (a, b) => { + for (const [key, value] of Object.entries(a)) { + if (key in b && JSON.stringify(b[key]) !== JSON.stringify(value)) { + return false + } + } + return true +} + /** * Issued by trusted authority (usually the one handling invocation) that attest * that specific UCAN delegation has been considered authentic. @@ -126,10 +214,10 @@ export const confirm = capability({ export const session = capability({ can: 'ucan/attest', // Should be web3.storage DID - with: URI.match({ protocol: 'did:' }), + with: Schema.DID, nb: Schema.struct({ // UCAN delegation that is being attested. - proof: Link, + proof: Schema.link(), }), }) @@ -200,38 +288,6 @@ function subsetsNbDelegations(claim, proof) { return true } -/** - * Checks that set of requested capabilities is a subset of the capabilities - * that had been allowed by the owner or the delegate. - * - * ⚠️ This function does not currently check that say `store/add` is allowed - * when say `store/*` was delegated, because it seems very unlikely that we - * will ever encounter delegations for `access/authorize` at all. - * - * @param {Schema.Infer[]} claim - * @param {Schema.Infer[]} proof - */ -const subsetCapabilities = (claim, proof) => { - const allowed = new Set(proof.map((p) => p.can)) - // If everything is allowed, no need to check further because it contains - // all the capabilities. - if (allowed.has('*')) { - return true - } - - // Otherwise we compute delta between what is allowed and what is requested. - const escalated = setDifference( - claim.map((c) => c.can), - allowed - ) - - if (escalated.size > 0) { - return new Failure(`unauthorized nb.att.can ${[...escalated].join(', ')}`) - } - - return true -} - /** * iterate delegated UCAN CIDs from an access/delegate capability.nb.delegations value. * diff --git a/packages/capabilities/src/customer.js b/packages/capabilities/src/customer.js new file mode 100644 index 000000000..eb761f76f --- /dev/null +++ b/packages/capabilities/src/customer.js @@ -0,0 +1,38 @@ +/** + * Consumer Capabilities + * + * These can be imported directly with: + * ```js + * import * as Provider from '@web3-storage/capabilities/consumer' + * ``` + * + * @module + */ +import * as Schema from './schema.js' + +/** + * Lists all subscriptions that provider has. If optional `customer` is provided + * then will only list subscriptions for that customer. + */ +export const list = Schema.capability({ + with: Schema.Provider, + can: 'customer/list', + nb: Schema.struct({ + customer: Schema.Account.optional(), + order: Schema.link({ version: 1 }).optional(), + }), +}) + +/** + * Adds a `customer` subscription to a provider. + */ +export const add = Schema.capability({ + with: Schema.Provider, + can: 'customer/add', + nb: Schema.struct({ + /** + * Must be a link to the signed `consumer/*` capability. + */ + provision: Schema.link({ version: 1 }), + }), +}) diff --git a/packages/capabilities/src/index.js b/packages/capabilities/src/index.js index 68c6ca9cd..2ec557a5a 100644 --- a/packages/capabilities/src/index.js +++ b/packages/capabilities/src/index.js @@ -6,8 +6,25 @@ import * as Upload from './upload.js' import * as Voucher from './voucher.js' import * as Access from './access.js' import * as Utils from './utils.js' +import * as Provision from './provision.js' +import * as Customer from './customer.js' +import * as Subscription from './subscription.js' +import * as Schema from './schema.js' -export { Access, Provider, Space, Top, Store, Upload, Voucher, Utils } +export { + Access, + Provider, + Provision, + Customer, + Subscription, + Space, + Top, + Store, + Upload, + Voucher, + Schema, + Utils, +} /** @type {import('./types.js').AbilitiesArray} */ export const abilitiesAsStrings = [ diff --git a/packages/capabilities/src/provider.js b/packages/capabilities/src/provider.js index 567dadc46..c539f3388 100644 --- a/packages/capabilities/src/provider.js +++ b/packages/capabilities/src/provider.js @@ -8,23 +8,18 @@ * * @module */ -import { capability, DID, struct } from '@ucanto/validator' import { equalWith, fail, equal } from './utils.js' - -// e.g. did:web:web3.storage or did:web:staging.web3.storage -export const Provider = DID.match({ method: 'web' }) - -export const AccountDID = DID.match({ method: 'mailto' }) +import * as Schema from './schema.js' /** * Capability can be invoked by an agent to add a provider to a space. */ -export const add = capability({ +export const add = Schema.capability({ can: 'provider/add', - with: AccountDID, - nb: struct({ - provider: Provider, - consumer: DID.match({ method: 'key' }), + with: Schema.Account, + nb: Schema.struct({ + provider: Schema.Provider, + consumer: Schema.Space, }), derives: (child, parent) => { return ( diff --git a/packages/capabilities/src/provision.js b/packages/capabilities/src/provision.js new file mode 100644 index 000000000..ebfd9106b --- /dev/null +++ b/packages/capabilities/src/provision.js @@ -0,0 +1,83 @@ +/** + * Consumer Capabilities + * + * These can be imported directly with: + * ```js + * import * as Provider from '@web3-storage/capabilities/consumer' + * ``` + * + * @module + */ +import * as Schema from './schema.js' +import { equalWith, fail, equal } from './utils.js' + +export const Provision = Schema.struct({ + /** + * Space DID capabilities are provisioned to by a provider. + */ + consumer: Schema.Space, + /** + * Account DID that + */ + customer: Schema.Account, + /** + * Order is a CID that identifies provider subscription. It is opaque + * identifier that can be used to enforce various constraints by the + * provider. For example provider could derive an order info from the user + * account and use it to enforce that only one space can be added per user. + */ + order: Schema.Order, +}) + +/** + * Capability provider delegates to a customer account when subscrpition is + * created. + */ +export const provision = Schema.capability({ + can: 'provision/*', + with: Schema.Provider, + nb: Schema.struct({ + consumer: Schema.Space.optional(), + order: Schema.Order, + customer: Schema.Account, + }), +}) + +/** + * Adds a consumer to a subscription. + * + * @see https://github.com/web3-storage/specs/blob/main/w3-provider.md#consumeradd-invocation + */ +export const add = Schema.capability({ + can: 'provision/add', + with: Schema.Provider, + nb: Provision, + derives: (child, parent) => { + return ( + fail(equalWith(child, parent)) || + fail(equal(child.nb.consumer, parent.nb.consumer, 'consumer')) || + fail(equal(child.nb.order, parent.nb.order, 'order')) || + true + ) + }, +}) + +/** + * Removes a consumer from the subscription. + */ +export const remove = Schema.capability({ + can: 'provision/remove', + with: Schema.Provider, + nb: Provision, +}) + +/** + * Lists consumers. + */ +export const list = Schema.capability({ + can: 'provision/list', + with: Schema.Provider, + nb: Schema.struct({ + order: Schema.Order.optional(), + }), +}) diff --git a/packages/capabilities/src/schema.js b/packages/capabilities/src/schema.js new file mode 100644 index 000000000..964118a76 --- /dev/null +++ b/packages/capabilities/src/schema.js @@ -0,0 +1,43 @@ +import { Schema, capability, DID, Failure } from '@ucanto/validator' + +export { Schema, capability, DID, Failure } + +export const { + literal, + struct, + dictionary, + link, + did, + string, + array, + boolean, + unknown, +} = Schema + +export const Bytes = unknown().refine({ + /** + * @param {unknown} value + */ + read(value) { + return value instanceof Uint8Array + ? value + : Schema.typeError({ + expect: 'Uint8Array', + actual: value, + }) + }, +}) + +export const Space = DID.match({ method: 'key' }) +export const Account = DID.match({ method: 'mailto' }) +export const Agent = Schema.did() + +/** + * We do not limit provider to a specific DID, because we want to allow it to + * be different per in dev, staging and prod. + */ +export const Provider = DID.match({ method: 'web' }) + +export const Order = Schema.link({ version: 1 }) + +export const Ability = Schema.string() diff --git a/packages/capabilities/src/subscription.js b/packages/capabilities/src/subscription.js new file mode 100644 index 000000000..9d36707d3 --- /dev/null +++ b/packages/capabilities/src/subscription.js @@ -0,0 +1,24 @@ +/** + * Consumer Capabilities + * + * These can be imported directly with: + * ```js + * import * as Provider from '@web3-storage/capabilities/consumer' + * ``` + * + * @module + */ +import * as Schema from './schema.js' + +/** + * Lists account subscriptions. Optional `provider` and `order` cane be + * specified to filter the results. + */ +export const list = Schema.capability({ + with: Schema.Account, + can: 'account/subscription/list', + nb: Schema.struct({ + provider: Schema.Provider.optional(), + order: Schema.link({ version: 1 }).optional(), + }), +}) diff --git a/packages/capabilities/src/types.js b/packages/capabilities/src/types.js new file mode 100644 index 000000000..42bb80886 --- /dev/null +++ b/packages/capabilities/src/types.js @@ -0,0 +1,2 @@ +// Only types are exported by types.ts file +export {} diff --git a/packages/capabilities/src/types.ts b/packages/capabilities/src/types.ts index 25442b907..dfc69481a 100644 --- a/packages/capabilities/src/types.ts +++ b/packages/capabilities/src/types.ts @@ -1,13 +1,19 @@ import type { TupleToUnion } from 'type-fest' import * as Ucanto from '@ucanto/interface' -import { InferInvokedCapability } from '@ucanto/interface' +import { InferInvokedCapability, Link, DID } from '@ucanto/interface' import { space, info, recover, recoverValidation } from './space.js' import * as provider from './provider.js' +import * as provision from './provision.js' +import * as subscription from './subscription.js' +import * as customer from './customer.js' import { top } from './top.js' import { add, list, remove, store } from './store.js' import * as UploadCaps from './upload.js' import { claim, redeem } from './voucher.js' import * as AccessCaps from './access.js' +import { Failure } from '@ucanto/validator/src/error.js' + +export type { Link } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface Unit {} @@ -22,12 +28,19 @@ export interface InsufficientStorage { // Access export type Access = InferInvokedCapability + +export type AccessRequest = InferInvokedCapability +export interface AccessRequestSuccess { + ran: Link +} +export interface AccessRequestFailure extends Failure {} + export type AccessAuthorize = InferInvokedCapability< typeof AccessCaps.authorize > +export interface AccessAuthorizeSuccess {} +export interface AccessAuthorizeFailure extends Failure {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export type AccessAuthorizeSuccess = Unit export type AccessClaim = InferInvokedCapability export interface AccessClaimSuccess { delegations: Record> @@ -46,14 +59,87 @@ export type AccessDelegateSuccess = unknown export type AccessDelegateFailure = { error: true } | InsufficientStorage export type AccessSession = InferInvokedCapability -export type AccessConfirm = InferInvokedCapability +export type AccessConfirm = InferInvokedCapability // Provider export type ProviderAdd = InferInvokedCapability -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ProviderAddSuccess {} export type ProviderAddFailure = Ucanto.Failure +// Provision + +export type Provision = InferInvokedCapability + +export type ProvisionAdd = InferInvokedCapability +export interface ProvisionAddSuccess {} +export interface ProvisionAddFailure extends Ucanto.Failure {} + +export type ProvisionRemove = InferInvokedCapability +export interface ProvisionRemoveSuccess {} +export interface ProvisionRemoveFailure extends Ucanto.Failure {} + +export type ProvisionList = InferInvokedCapability +export interface ProvisionListSuccess {} +export interface ProvisionListFailure extends Ucanto.Failure {} + +// Subscription + +export type SubscriptionList = InferInvokedCapability +export interface SubscriptionListSuccess { + // we will want to add add fields like other list operations + // but for now this will do. + results: SubscriptionRecord[] +} +export interface SubscriptionListFailure extends Ucanto.Failure {} + +// Customer + +export type CustomerAdd = InferInvokedCapability +export interface CustomerAddSuccess { + // Link of the invocation that successfully created a customer subscription + cause: Link +} + +export interface CustomerAddFailure extends Failure {} +export type CustomerList = InferInvokedCapability + +export interface Subscription { + /** + * CID of the `consumer/*` delegation that grants customer access to this + * subscription. + */ + provision: Link + /** + * CID of the invocation that created this subscription + */ + cause: Link + /** + * Account that is billed for the subscription. + */ + customer: DID + /** + * Provider that provides service to the customer. + */ + provider: DID<'web'> + /** + * Identifier generated by the provider to identify this subscription. + */ + order: Link +} + +export interface SubscriptionRecord extends Subscription { + inserted_at: Date + updated_at: Date +} + +export interface CustomerListSuccess { + // we will want to add add fields like other list operations + // but for now this will do. + results: SubscriptionRecord[] +} + +export interface CustomerListFailure extends Ucanto.Failure {} + // Space export type Space = InferInvokedCapability export type SpaceInfo = InferInvokedCapability diff --git a/packages/capabilities/test/capabilities/access.test.js b/packages/capabilities/test/capabilities/access.test.js index f8069db30..5e716213e 100644 --- a/packages/capabilities/test/capabilities/access.test.js +++ b/packages/capabilities/test/capabilities/access.test.js @@ -6,22 +6,28 @@ import { alice, bob, service, mallory } from '../helpers/fixtures.js' import * as Ucanto from '@ucanto/interface' import { delegate, invoke, parseLink } from '@ucanto/core' -describe('access capabilities', function () { - describe('access/authorize', function () { +const w3 = service.withDID('did:web:test.web3.storage') + +describe.only('access capabilities', function () { + describe('access/request', function () { it('should self issue', async function () { const agent = mallory - const auth = Access.authorize.invoke({ + const auth = Access.request.invoke({ issuer: agent, audience: service, with: agent.did(), nb: { - iss: 'did:mailto:web3.storage:test', - att: [{ can: '*' }], + from: 'did:mailto:web3.storage:test', + access: { + 'ucan:*': { + '*': [], + }, + }, }, }) const result = await access(await auth.delegate(), { - capability: Access.authorize, + capability: Access.request, principal: Verifier, authority: service, }) @@ -29,39 +35,47 @@ describe('access capabilities', function () { assert.fail('error in self issue') } else { assert.deepEqual(result.audience.did(), service.did()) - assert.equal(result.capability.can, 'access/authorize') + assert.equal(result.capability.can, 'access/request') assert.deepEqual(result.capability.nb, { - iss: 'did:mailto:web3.storage:test', - att: [{ can: '*' }], + from: 'did:mailto:web3.storage:test', + access: { + 'ucan:*': { + '*': [], + }, + }, }) } }) - it('should delegate from authorize to authorize', async function () { + it('should delegate from access/request to access/request', async function () { const agent1 = bob const agent2 = mallory - const claim = Access.authorize.invoke({ + const claim = Access.request.invoke({ issuer: agent2, audience: service, with: agent1.did(), nb: { - iss: 'did:mailto:web3.storage:test', - att: [{ can: '*' }], + from: 'did:mailto:web3.storage:test', + access: { + 'ucan:*': { + '*': [], + }, + }, }, proofs: [ - await Access.authorize.delegate({ + await Access.request.delegate({ issuer: agent1, audience: agent2, with: agent1.did(), nb: { - iss: 'did:mailto:web3.storage:test', + from: 'did:mailto:web3.storage:test', }, }), ], }) const result = await access(await claim.delegate(), { - capability: Access.authorize, + capability: Access.request, principal: Verifier, authority: service, }) @@ -70,24 +84,28 @@ describe('access capabilities', function () { assert.fail('should not error') } else { assert.deepEqual(result.audience.did(), service.did()) - assert.equal(result.capability.can, 'access/authorize') + assert.equal(result.capability.can, 'access/request') assert.deepEqual(result.capability.nb, { - iss: 'did:mailto:web3.storage:test', - att: [{ can: '*' }], + from: 'did:mailto:web3.storage:test', + access: { 'ucan:*': { '*': [] } }, }) } }) - it('should delegate from authorize/* to authorize', async function () { + it('should delegate from access/* to access/request', async function () { const agent1 = bob const agent2 = mallory - const claim = Access.authorize.invoke({ + const claim = Access.request.invoke({ issuer: agent2, audience: service, with: agent1.did(), nb: { - iss: 'did:mailto:web3.storage:test', - att: [{ can: '*' }], + from: 'did:mailto:web3.storage:test', + access: { + 'ucan:*': { + '*': [], + }, + }, }, proofs: [ await Access.access.delegate({ @@ -99,7 +117,7 @@ describe('access capabilities', function () { }) const result = await access(await claim.delegate(), { - capability: Access.authorize, + capability: Access.request, principal: Verifier, authority: service, }) @@ -108,24 +126,32 @@ describe('access capabilities', function () { assert.fail('should not error') } else { assert.deepEqual(result.audience.did(), service.did()) - assert.equal(result.capability.can, 'access/authorize') + assert.equal(result.capability.can, 'access/request') assert.deepEqual(result.capability.nb, { - iss: 'did:mailto:web3.storage:test', - att: [{ can: '*' }], + from: 'did:mailto:web3.storage:test', + access: { + 'ucan:*': { + '*': [], + }, + }, }) } }) - it('should delegate from * to authorize', async function () { + it('should delegate from * to access/request', async function () { const agent1 = bob const agent2 = mallory - const claim = Access.authorize.invoke({ + const claim = Access.request.invoke({ issuer: agent2, audience: service, with: agent1.did(), nb: { - iss: 'did:mailto:web3.storage:test', - att: [{ can: '*' }], + from: 'did:mailto:web3.storage:test', + access: { + 'ucan:*': { + '*': [], + }, + }, }, proofs: [ await Access.top.delegate({ @@ -137,7 +163,7 @@ describe('access capabilities', function () { }) const result = await access(await claim.delegate(), { - capability: Access.authorize, + capability: Access.request, principal: Verifier, authority: service, }) @@ -146,39 +172,47 @@ describe('access capabilities', function () { assert.fail('should not error') } else { assert.deepEqual(result.audience.did(), service.did()) - assert.equal(result.capability.can, 'access/authorize') + assert.equal(result.capability.can, 'access/request') assert.deepEqual(result.capability.nb, { - iss: 'did:mailto:web3.storage:test', - att: [{ can: '*' }], + from: 'did:mailto:web3.storage:test', + access: { + 'ucan:*': { + '*': [], + }, + }, }) } }) - it('should error auth to auth when `iss` is different', async function () { + it('should error access/request when `from` is different', async function () { const agent1 = bob const agent2 = mallory - const claim = Access.authorize.invoke({ + const claim = Access.request.invoke({ issuer: agent2, audience: service, with: agent1.did(), nb: { - iss: 'did:mailto:web3.storage:ANOTHER_TEST', - att: [{ can: '*' }], + from: 'did:mailto:web3.storage:ANOTHER_TEST', + access: { + 'ucan:*': { + '*': [], + }, + }, }, proofs: [ - await Access.authorize.delegate({ + await Access.request.delegate({ issuer: agent1, audience: agent2, with: agent1.did(), nb: { - iss: 'did:mailto:web3.storage:test', + from: 'did:mailto:web3.storage:test', }, }), ], }) const result = await access(await claim.delegate(), { - capability: Access.authorize, + capability: Access.request, principal: Verifier, authority: service, }) @@ -191,29 +225,37 @@ describe('access capabilities', function () { }) it('should be able to derive from * scope', async function () { - const claim = Access.authorize.invoke({ + const claim = Access.request.invoke({ issuer: bob, audience: service, with: alice.did(), nb: { - iss: 'did:mailto:web.mail:alice', - att: [{ can: 'store/*' }], + from: 'did:mailto:web.mail:alice', + access: { + 'ucan:*': { + 'store/*': [], + }, + }, }, proofs: [ - await Access.authorize.delegate({ + await Access.request.delegate({ issuer: alice, audience: bob, with: alice.did(), nb: { - iss: 'did:mailto:web.mail:alice', - att: [{ can: '*' }], + from: 'did:mailto:web.mail:alice', + access: { + 'ucan:*': { + '*': [], + }, + }, }, }), ], }) const result = await access(await claim.delegate(), { - capability: Access.authorize, + capability: Access.request, principal: Verifier, authority: service, }) @@ -222,29 +264,38 @@ describe('access capabilities', function () { }) it('should be able to reduce scope', async function () { - const claim = Access.authorize.invoke({ + const claim = Access.request.invoke({ issuer: bob, audience: service, with: alice.did(), nb: { - iss: 'did:mailto:web.mail:alice', - att: [{ can: 'store/add' }], + from: 'did:mailto:web.mail:alice', + access: { + 'ucan:*': { + 'store/add': [], + }, + }, }, proofs: [ - await Access.authorize.delegate({ + await Access.request.delegate({ issuer: alice, audience: bob, with: alice.did(), nb: { - iss: 'did:mailto:web.mail:alice', - att: [{ can: 'store/add' }, { can: 'store/remove' }], + from: 'did:mailto:web.mail:alice', + access: { + 'ucan:*': { + 'store/add': [], + 'store/remove': [], + }, + }, }, }), ], }) const result = await access(await claim.delegate(), { - capability: Access.authorize, + capability: Access.request, principal: Verifier, authority: service, }) @@ -253,35 +304,43 @@ describe('access capabilities', function () { }) it('should error on escalation', async function () { - const claim = Access.authorize.invoke({ + const claim = Access.request.invoke({ issuer: bob, audience: service, with: alice.did(), nb: { - iss: 'did:mailto:web.mail:alice', - att: [{ can: '*' }], + from: 'did:mailto:web.mail:alice', + access: { + 'ucan:*': { + '*': [], + }, + }, }, proofs: [ - await Access.authorize.delegate({ + await Access.request.delegate({ issuer: alice, audience: bob, with: alice.did(), nb: { - iss: 'did:mailto:web.mail:alice', - att: [{ can: 'store/*' }], + from: 'did:mailto:web.mail:alice', + access: { + 'ucan:*': { + 'store/*': [], + }, + }, }, }), ], }) const result = await access(await claim.delegate(), { - capability: Access.authorize, + capability: Access.request, principal: Verifier, authority: service, }) if (result.error) { - assert.ok(result.message.includes('unauthorized nb.att.can *')) + assert.ok(result.message.includes('ability "*"')) } else { assert.fail('should error') } @@ -290,13 +349,17 @@ describe('access capabilities', function () { it('should error on principal misalignment', async function () { const agent1 = bob const agent2 = mallory - const claim = Access.authorize.invoke({ + const claim = Access.request.invoke({ issuer: agent2, audience: service, with: alice.did(), nb: { - iss: 'did:mailto:web3.storage:test', - att: [{ can: '*' }], + from: 'did:mailto:web3.storage:test', + access: { + 'ucan:': { + '*': [], + }, + }, }, proofs: [ await Access.top.delegate({ @@ -308,7 +371,7 @@ describe('access capabilities', function () { }) const result = await access(await claim.delegate(), { - capability: Access.authorize, + capability: Access.request, principal: Verifier, authority: service, }) @@ -322,80 +385,108 @@ describe('access capabilities', function () { it('should fail validation if its not mailto', async function () { assert.throws(() => { - Access.authorize.invoke({ + Access.request.invoke({ issuer: bob, audience: service, with: bob.did(), nb: { // @ts-expect-error - iss: 'did:NOT_MAILTO:web3.storage:test', - att: [{ can: '*' }], + from: 'did:NOT_MAILTO:web3.storage:test', + access: { + 'ucan:*': { + '*': [], + }, + }, }, }) }, /Expected a did:mailto: but got "did:NOT_MAILTO:web3.storage:test" instead/) }) }) - describe('access/confirm', function () { - it('should self issue', async function () { - const agent = mallory - const ucan = Access.confirm.invoke({ - issuer: agent, - audience: service, - with: agent.did(), + describe('access/authorize', function () { + it('must be issued by a provider', async function () { + try { + Access.authorize.invoke({ + issuer: alice, + audience: bob, + // @ts-expect-error - must be a provided did + with: alice.did(), + nb: { + from: 'did:mailto:web3.storage:test', + to: bob.did(), + access: { + 'ucan:*': { + '*': [], + }, + }, + }, + }) + assert.fail('should have failed') + } catch (error) { + assert.match(String(error), /Expected a did:web: but got "did:key/) + } + }) + + it('can be issued by a service', async () => { + const auth = Access.authorize.invoke({ + issuer: w3, + audience: alice, + with: w3.did(), nb: { - iss: 'did:mailto:web3.storage:test', - aud: agent.did(), - att: [{ can: '*' }], + from: 'did:mailto:web3.storage:test', + to: alice.did(), + access: { + 'ucan:*': { + '*': [], + }, + }, }, }) - const result = await access(await ucan.delegate(), { - capability: Access.confirm, + const result = await access(await auth.delegate(), { + capability: Access.authorize, principal: Verifier, - authority: service, + authority: w3, }) - if (result.error) { - assert.fail('error in self issue') - } else { - assert.deepEqual(result.audience.did(), service.did()) - assert.equal(result.capability.can, 'access/confirm') - assert.deepEqual(result.capability.nb, { - iss: 'did:mailto:web3.storage:test', - aud: agent.did(), - att: [{ can: '*' }], - }) - } + + assert.equal(result.error, undefined) }) - it('should delegate from confirm to confirm', async function () { - const agent1 = bob - const agent2 = mallory - const ucan = Access.confirm.invoke({ - issuer: agent2, - audience: service, - with: agent1.did(), + it('can be delegated to an agent', async function () { + const ucan = Access.authorize.invoke({ + issuer: alice, + audience: bob, + with: w3.did(), nb: { - iss: 'did:mailto:web3.storage:test', - aud: agent2.did(), - att: [{ can: '*' }], + from: 'did:mailto:web3.storage:test', + to: bob.did(), + access: { + 'ucan:*': { + '*': [], + }, + }, }, proofs: [ - await Access.confirm.delegate({ - issuer: agent1, - audience: agent2, - with: agent1.did(), + await Access.authorize.delegate({ + issuer: w3, + audience: alice, + with: w3.did(), nb: { - iss: 'did:mailto:web3.storage:test', + from: 'did:mailto:web3.storage:test', + access: { + 'ucan:*': { + '*': [], + }, + }, }, }), ], }) const result = await access(await ucan.delegate(), { - capability: Access.confirm, + capability: Access.authorize, principal: Verifier, - authority: service, + authority: w3, }) if (result.error) { @@ -405,8 +496,13 @@ describe('access capabilities', function () { assert.equal(result.capability.can, 'access/confirm') assert.deepEqual(result.capability.nb, { iss: 'did:mailto:web3.storage:test', - aud: agent2.did(), - att: [{ can: '*' }], + from: 'did:mailto:web3.storage:test', + to: bob.did(), + access: { + 'ucan:*': { + '*': [], + }, + }, }) } }) @@ -414,7 +510,7 @@ describe('access capabilities', function () { it('should delegate from access/* to access/confirm', async function () { const agent1 = bob const agent2 = mallory - const ucan = Access.confirm.invoke({ + const ucan = Access.authorize.invoke({ issuer: agent2, audience: service, with: agent1.did(), @@ -433,7 +529,7 @@ describe('access capabilities', function () { }) const result = await access(await ucan.delegate(), { - capability: Access.confirm, + capability: Access.authorize, principal: Verifier, authority: service, }) @@ -454,7 +550,7 @@ describe('access capabilities', function () { it('should delegate from * to access/confirm', async function () { const agent1 = bob const agent2 = mallory - const ucan = Access.confirm.invoke({ + const ucan = Access.authorize.invoke({ issuer: agent2, audience: service, with: agent1.did(), @@ -473,7 +569,7 @@ describe('access capabilities', function () { }) const result = await access(await ucan.delegate(), { - capability: Access.confirm, + capability: Access.authorize, principal: Verifier, authority: service, }) @@ -494,7 +590,7 @@ describe('access capabilities', function () { it('should error when `iss` is different', async function () { const agent1 = bob const agent2 = mallory - const ucan = Access.confirm.invoke({ + const ucan = Access.authorize.invoke({ issuer: agent2, audience: service, with: agent1.did(), @@ -504,7 +600,7 @@ describe('access capabilities', function () { att: [{ can: '*' }], }, proofs: [ - await Access.confirm.delegate({ + await Access.authorize.delegate({ issuer: agent1, audience: agent2, with: agent1.did(), @@ -516,7 +612,7 @@ describe('access capabilities', function () { }) const result = await access(await ucan.delegate(), { - capability: Access.confirm, + capability: Access.authorize, principal: Verifier, authority: service, }) @@ -529,7 +625,7 @@ describe('access capabilities', function () { }) it('should be able to derive from * scope', async function () { - const ucan = Access.confirm.invoke({ + const ucan = Access.authorize.invoke({ issuer: bob, audience: service, with: alice.did(), @@ -539,7 +635,7 @@ describe('access capabilities', function () { att: [{ can: 'store/*' }], }, proofs: [ - await Access.confirm.delegate({ + await Access.authorize.delegate({ issuer: alice, audience: bob, with: alice.did(), @@ -552,7 +648,7 @@ describe('access capabilities', function () { }) const result = await access(await ucan.delegate(), { - capability: Access.confirm, + capability: Access.authorize, principal: Verifier, authority: service, }) @@ -561,7 +657,7 @@ describe('access capabilities', function () { }) it('should be able to reduce scope', async function () { - const ucan = Access.confirm.invoke({ + const ucan = Access.authorize.invoke({ issuer: bob, audience: service, with: alice.did(), @@ -571,7 +667,7 @@ describe('access capabilities', function () { att: [{ can: 'store/add' }], }, proofs: [ - await Access.confirm.delegate({ + await Access.authorize.delegate({ issuer: alice, audience: bob, with: alice.did(), @@ -584,7 +680,7 @@ describe('access capabilities', function () { }) const result = await access(await ucan.delegate(), { - capability: Access.confirm, + capability: Access.authorize, principal: Verifier, authority: service, }) @@ -593,7 +689,7 @@ describe('access capabilities', function () { }) it('should error on escalation', async function () { - const ucan = Access.confirm.invoke({ + const ucan = Access.authorize.invoke({ issuer: bob, audience: service, with: alice.did(), @@ -603,7 +699,7 @@ describe('access capabilities', function () { att: [{ can: '*' }], }, proofs: [ - await Access.confirm.delegate({ + await Access.authorize.delegate({ issuer: alice, audience: bob, with: alice.did(), @@ -616,7 +712,7 @@ describe('access capabilities', function () { }) const result = await access(await ucan.delegate(), { - capability: Access.confirm, + capability: Access.authorize, principal: Verifier, authority: service, }) @@ -631,7 +727,7 @@ describe('access capabilities', function () { it('should error on principal misalignment', async function () { const agent1 = bob const agent2 = mallory - const ucan = Access.confirm.invoke({ + const ucan = Access.authorize.invoke({ issuer: agent2, audience: service, with: alice.did(), @@ -650,7 +746,7 @@ describe('access capabilities', function () { }) const result = await access(await ucan.delegate(), { - capability: Access.confirm, + capability: Access.authorize, principal: Verifier, authority: service, }) @@ -664,7 +760,7 @@ describe('access capabilities', function () { it('should fail validation if its not mailto', async function () { assert.throws(() => { - Access.confirm.invoke({ + Access.authorize.invoke({ issuer: bob, audience: service, with: bob.did(), @@ -679,9 +775,9 @@ describe('access capabilities', function () { }) }) - describe('access/claim', () => { + describe.skip('access/claim', () => { // ensure we can use the capability to produce the invocations from the spec at https://github.com/web3-storage/specs/blob/576b988fb7cfa60049611963179277c420605842/w3-access.md - it('can create/access delegations from spec', async () => { + it('create access/authorize delegations from spec', async () => { const audience = service.withDID('did:web:web3.storage') const examples = [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4fcc00e4c..af97f304b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: 5.4 +lockfileVersion: 5.3 importers: @@ -9,7 +9,6 @@ importers: docusaurus-plugin-typedoc: ^0.18.0 lint-staged: ^13.1.0 prettier: 2.8.3 - simple-git-hooks: ^2.8.1 typedoc: ^0.23.22 typedoc-plugin-markdown: ^3.14.0 typedoc-plugin-missing-exports: ^1.0.0 @@ -21,10 +20,9 @@ importers: typedoc-plugin-missing-exports: 1.0.0_typedoc@0.23.24 devDependencies: '@docusaurus/core': 2.3.0_typescript@4.9.5 - docusaurus-plugin-typedoc: 0.18.0_res3k6jdwbtxmimicswazugz6i + docusaurus-plugin-typedoc: 0.18.0_8925b57923b06776218814ac0cd0d9f2 lint-staged: 13.1.0 prettier: 2.8.3 - simple-git-hooks: 2.8.1 typedoc-plugin-markdown: 3.14.0_typedoc@0.23.24 typescript: 4.9.5 wrangler: 2.9.0 @@ -1778,11 +1776,6 @@ packages: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true dependencies: '@babel/core': 7.20.12 '@babel/generator': 7.20.14 @@ -1804,7 +1797,7 @@ packages: '@slorber/static-site-generator-webpack-plugin': 4.0.7 '@svgr/webpack': 6.5.1 autoprefixer: 10.4.13_postcss@8.4.21 - babel-loader: 8.3.0_la66t7xldg4uecmyawueag5wkm + babel-loader: 8.3.0_583de9feeb19b942099805a8401bb653 babel-plugin-dynamic-import-node: 2.3.3 boxen: 6.2.1 chalk: 4.1.2 @@ -1816,7 +1809,7 @@ packages: copy-webpack-plugin: 11.0.0_webpack@5.75.0 core-js: 3.27.2 css-loader: 6.7.3_webpack@5.75.0 - css-minimizer-webpack-plugin: 4.2.2_dpcjkp5o5ztxuvt4quwwvenemi + css-minimizer-webpack-plugin: 4.2.2_clean-css@5.3.2+webpack@5.75.0 cssnano: 5.1.14_postcss@8.4.21 del: 6.1.1 detect-port: 1.5.1 @@ -1832,12 +1825,12 @@ packages: lodash: 4.17.21 mini-css-extract-plugin: 2.7.2_webpack@5.75.0 postcss: 8.4.21 - postcss-loader: 7.0.2_6jdsrmfenkuhhw3gx4zvjlznce + postcss-loader: 7.0.2_postcss@8.4.21+webpack@5.75.0 prompts: 2.4.2 - react-dev-utils: 12.0.1_hhrrucqyg4eysmfpujvov2ym5u + react-dev-utils: 12.0.1_typescript@4.9.5+webpack@5.75.0 react-helmet-async: 1.3.0 react-loadable: /@docusaurus/react-loadable/5.5.2 - react-loadable-ssr-addon-v5-slorber: 1.0.1_pwfl7zyferpbeh35vaepqxwaky + react-loadable-ssr-addon-v5-slorber: 1.0.1_7d8abfe705245e121f7da808f85ec056 react-router: 5.3.4 react-router-config: 5.1.1_react-router@5.3.4 react-router-dom: 5.3.4 @@ -1848,7 +1841,7 @@ packages: terser-webpack-plugin: 5.3.6_webpack@5.75.0 tslib: 2.5.0 update-notifier: 5.1.0 - url-loader: 4.1.1_p5dl6emkcwslbw72e37w4ug7em + url-loader: 4.1.1_file-loader@6.2.0+webpack@5.75.0 wait-on: 6.0.1 webpack: 5.75.0 webpack-bundle-analyzer: 4.7.0 @@ -1898,11 +1891,6 @@ packages: peerDependencies: react: ^16.8.4 || ^17.0.0 react-dom: ^16.8.4 || ^17.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true dependencies: '@babel/parser': 7.20.13 '@babel/traverse': 7.20.13 @@ -1919,7 +1907,7 @@ packages: tslib: 2.5.0 unified: 9.2.2 unist-util-visit: 2.0.3 - url-loader: 4.1.1_p5dl6emkcwslbw72e37w4ug7em + url-loader: 4.1.1_file-loader@6.2.0+webpack@5.75.0 webpack: 5.75.0 transitivePeerDependencies: - '@docusaurus/types' @@ -1934,9 +1922,6 @@ packages: resolution: {integrity: sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==} peerDependencies: react: '*' - peerDependenciesMeta: - react: - optional: true dependencies: '@types/react': 18.0.27 prop-types: 15.8.1 @@ -1995,7 +1980,7 @@ packages: resolve-pathname: 3.0.0 shelljs: 0.8.5 tslib: 2.5.0 - url-loader: 4.1.1_p5dl6emkcwslbw72e37w4ug7em + url-loader: 4.1.1_file-loader@6.2.0+webpack@5.75.0 webpack: 5.75.0 transitivePeerDependencies: - '@swc/core' @@ -3430,7 +3415,7 @@ packages: '@types/yargs-parser': 21.0.0 dev: true - /@typescript-eslint/eslint-plugin/5.50.0_go4drrxstycfikanvu45pi4vgq: + /@typescript-eslint/eslint-plugin/5.50.0_33b838c6f29e0454280dad39d7a39534: resolution: {integrity: sha512-vwksQWSFZiUhgq3Kv7o1Jcj0DUNylwnIlGvKvLLYsq8pAWha6/WCnXUeaSoNNha/K7QSf2+jvmkxggC1u3pIwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -3441,10 +3426,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.50.0_4vsywjlpuriuw3tl5oq6zy5a64 + '@typescript-eslint/parser': 5.50.0_eslint@8.33.0+typescript@4.9.5 '@typescript-eslint/scope-manager': 5.50.0 - '@typescript-eslint/type-utils': 5.50.0_4vsywjlpuriuw3tl5oq6zy5a64 - '@typescript-eslint/utils': 5.50.0_4vsywjlpuriuw3tl5oq6zy5a64 + '@typescript-eslint/type-utils': 5.50.0_eslint@8.33.0+typescript@4.9.5 + '@typescript-eslint/utils': 5.50.0_eslint@8.33.0+typescript@4.9.5 debug: 4.3.4 eslint: 8.33.0 grapheme-splitter: 1.0.4 @@ -3458,20 +3443,20 @@ packages: - supports-color dev: true - /@typescript-eslint/experimental-utils/5.50.0_4vsywjlpuriuw3tl5oq6zy5a64: + /@typescript-eslint/experimental-utils/5.50.0_eslint@8.33.0+typescript@4.9.5: resolution: {integrity: sha512-gZIhzNRivy0RVqcxjKnQ+ipGc0qolilhBeNmvH+Dvu7Vymug+IfiYxTj2zM7mIlHsw6Q5aH7L7WmuTE3tZyzag==} 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.50.0_4vsywjlpuriuw3tl5oq6zy5a64 + '@typescript-eslint/utils': 5.50.0_eslint@8.33.0+typescript@4.9.5 eslint: 8.33.0 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/parser/5.50.0_4vsywjlpuriuw3tl5oq6zy5a64: + /@typescript-eslint/parser/5.50.0_eslint@8.33.0+typescript@4.9.5: resolution: {integrity: sha512-KCcSyNaogUDftK2G9RXfQyOCt51uB5yqC6pkUYqhYh8Kgt+DwR5M0EwEAxGPy/+DH6hnmKeGsNhiZRQxjH71uQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -3499,7 +3484,7 @@ packages: '@typescript-eslint/visitor-keys': 5.50.0 dev: true - /@typescript-eslint/type-utils/5.50.0_4vsywjlpuriuw3tl5oq6zy5a64: + /@typescript-eslint/type-utils/5.50.0_eslint@8.33.0+typescript@4.9.5: resolution: {integrity: sha512-dcnXfZ6OGrNCO7E5UY/i0ktHb7Yx1fV6fnQGGrlnfDhilcs6n19eIRcvLBqx6OQkrPaFlDPk3OJ0WlzQfrV0bQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -3510,7 +3495,7 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 5.50.0_typescript@4.9.5 - '@typescript-eslint/utils': 5.50.0_4vsywjlpuriuw3tl5oq6zy5a64 + '@typescript-eslint/utils': 5.50.0_eslint@8.33.0+typescript@4.9.5 debug: 4.3.4 eslint: 8.33.0 tsutils: 3.21.0_typescript@4.9.5 @@ -3545,7 +3530,7 @@ packages: - supports-color dev: true - /@typescript-eslint/utils/5.50.0_4vsywjlpuriuw3tl5oq6zy5a64: + /@typescript-eslint/utils/5.50.0_eslint@8.33.0+typescript@4.9.5: resolution: {integrity: sha512-v/AnUFImmh8G4PH0NDkf6wA8hujNNcrwtecqW4vtQ1UOSNBaZl49zP1SHoZ/06e+UiwzHpgb5zP5+hwlYYWYAw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -4253,7 +4238,7 @@ packages: - debug dev: true - /babel-loader/8.3.0_la66t7xldg4uecmyawueag5wkm: + /babel-loader/8.3.0_583de9feeb19b942099805a8401bb653: resolution: {integrity: sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==} engines: {node: '>= 8.9'} peerDependencies: @@ -4420,8 +4405,6 @@ packages: raw-body: 2.5.1 type-is: 1.6.18 unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color dev: true /bonjour-service/1.1.0: @@ -4955,8 +4938,6 @@ packages: on-headers: 1.0.2 safe-buffer: 5.1.2 vary: 1.1.2 - transitivePeerDependencies: - - supports-color dev: true /concat-map/0.0.1: @@ -5198,7 +5179,7 @@ packages: webpack: 5.75.0 dev: true - /css-minimizer-webpack-plugin/4.2.2_dpcjkp5o5ztxuvt4quwwvenemi: + /css-minimizer-webpack-plugin/4.2.2_clean-css@5.3.2+webpack@5.75.0: resolution: {integrity: sha512-s3Of/4jKfw1Hj9CxEO1E5oXhQAxlayuHO2y/ML+C6I9sQ7FdzfEV6QgMLN3vI+qFsjJGIAFLKtQK7t8BOXAIyA==} engines: {node: '>= 14.15.0'} peerDependencies: @@ -5375,22 +5356,12 @@ packages: /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true dependencies: ms: 2.0.0 dev: true /debug/3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true dependencies: ms: 2.1.3 dev: true @@ -5622,8 +5593,6 @@ packages: dependencies: address: 1.2.2 debug: 2.6.9 - transitivePeerDependencies: - - supports-color dev: true /detect-port/1.5.1: @@ -5677,7 +5646,7 @@ packages: esutils: 2.0.3 dev: true - /docusaurus-plugin-typedoc/0.18.0_res3k6jdwbtxmimicswazugz6i: + /docusaurus-plugin-typedoc/0.18.0_8925b57923b06776218814ac0cd0d9f2: resolution: {integrity: sha512-kurIUu8LhVIOPT88HoeBcu0/D2GMDdg0pUYaFlqeuXT9an6Wlgvuy0C22ZMYcJUcp/gA/Mw2XdUHubsLK2M4uA==} peerDependencies: typedoc: '>=0.23.0' @@ -6242,7 +6211,7 @@ packages: eslint: 8.33.0 dev: true - /eslint-config-standard-with-typescript/30.0.0_frfgwa7fqjzszldru3sxumpviq: + /eslint-config-standard-with-typescript/30.0.0_2c4a6b03e582732cac71a6e57a31f544: resolution: {integrity: sha512-/Ltst1BCZCWrGmqprLHBkTwuAbcoQrR8uMeSzZAv1vHKIVg+2nFje+DULA30SW01yCNhnx0a8yhZBkR0ZZPp+w==} peerDependencies: '@typescript-eslint/eslint-plugin': ^5.0.0 @@ -6252,11 +6221,11 @@ packages: eslint-plugin-promise: ^6.0.0 typescript: '*' dependencies: - '@typescript-eslint/eslint-plugin': 5.50.0_go4drrxstycfikanvu45pi4vgq - '@typescript-eslint/parser': 5.50.0_4vsywjlpuriuw3tl5oq6zy5a64 + '@typescript-eslint/eslint-plugin': 5.50.0_33b838c6f29e0454280dad39d7a39534 + '@typescript-eslint/parser': 5.50.0_eslint@8.33.0+typescript@4.9.5 eslint: 8.33.0 - eslint-config-standard: 17.0.0_xh3wrndcszbt2l7hdksdjqnjcq - eslint-plugin-import: 2.27.5_ufewo3pl5nnmz6lltvjrdi2hii + eslint-config-standard: 17.0.0_b9f768b46296433d2fe71aa434c1a914 + eslint-plugin-import: 2.27.5_eslint@8.33.0 eslint-plugin-n: 15.6.1_eslint@8.33.0 eslint-plugin-promise: 6.1.1_eslint@8.33.0 typescript: 4.9.5 @@ -6264,7 +6233,7 @@ packages: - supports-color dev: true - /eslint-config-standard/17.0.0_xh3wrndcszbt2l7hdksdjqnjcq: + /eslint-config-standard/17.0.0_b9f768b46296433d2fe71aa434c1a914: resolution: {integrity: sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==} peerDependencies: eslint: ^8.0.1 @@ -6273,21 +6242,21 @@ packages: eslint-plugin-promise: ^6.0.0 dependencies: eslint: 8.33.0 - eslint-plugin-import: 2.27.5_ufewo3pl5nnmz6lltvjrdi2hii + eslint-plugin-import: 2.27.5_eslint@8.33.0 eslint-plugin-n: 15.6.1_eslint@8.33.0 eslint-plugin-promise: 6.1.1_eslint@8.33.0 dev: true - /eslint-etc/5.2.0_4vsywjlpuriuw3tl5oq6zy5a64: + /eslint-etc/5.2.0_eslint@8.33.0+typescript@4.9.5: resolution: {integrity: sha512-Gcm/NMa349FOXb1PEEfNMMyIANuorIc2/mI5Vfu1zENNsz+FBVhF62uY6gPUCigm/xDOc8JOnl+71WGnlzlDag==} peerDependencies: eslint: ^8.0.0 typescript: ^4.0.0 dependencies: - '@typescript-eslint/experimental-utils': 5.50.0_4vsywjlpuriuw3tl5oq6zy5a64 + '@typescript-eslint/experimental-utils': 5.50.0_eslint@8.33.0+typescript@4.9.5 eslint: 8.33.0 tsutils: 3.21.0_typescript@4.9.5 - tsutils-etc: 1.4.1_dw2ve3pa3py4wrhanasku2jsqi + tsutils-etc: 1.4.1_tsutils@3.21.0+typescript@4.9.5 typescript: 4.9.5 transitivePeerDependencies: - supports-color @@ -6299,37 +6268,19 @@ packages: debug: 3.2.7 is-core-module: 2.11.0 resolve: 1.22.1 - transitivePeerDependencies: - - supports-color dev: true - /eslint-module-utils/2.7.4_ypqpzq5szckeh62pb722iz7nn4: + /eslint-module-utils/2.7.4_eslint@8.33.0: resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} peerDependencies: - '@typescript-eslint/parser': '*' eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true eslint: optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true dependencies: - '@typescript-eslint/parser': 5.50.0_4vsywjlpuriuw3tl5oq6zy5a64 debug: 3.2.7 eslint: 8.33.0 - eslint-import-resolver-node: 0.3.7 - transitivePeerDependencies: - - supports-color dev: true /eslint-plugin-es/4.1.0_eslint@8.33.0: @@ -6343,16 +6294,16 @@ packages: regexpp: 3.2.0 dev: true - /eslint-plugin-etc/2.0.2_4vsywjlpuriuw3tl5oq6zy5a64: + /eslint-plugin-etc/2.0.2_eslint@8.33.0+typescript@4.9.5: 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.5 - '@typescript-eslint/experimental-utils': 5.50.0_4vsywjlpuriuw3tl5oq6zy5a64 + '@typescript-eslint/experimental-utils': 5.50.0_eslint@8.33.0+typescript@4.9.5 eslint: 8.33.0 - eslint-etc: 5.2.0_4vsywjlpuriuw3tl5oq6zy5a64 + eslint-etc: 5.2.0_eslint@8.33.0+typescript@4.9.5 requireindex: 1.2.0 tslib: 2.5.0 tsutils: 3.21.0_typescript@4.9.5 @@ -6361,17 +6312,12 @@ packages: - supports-color dev: true - /eslint-plugin-import/2.27.5_ufewo3pl5nnmz6lltvjrdi2hii: + /eslint-plugin-import/2.27.5_eslint@8.33.0: resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} peerDependencies: - '@typescript-eslint/parser': '*' eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true dependencies: - '@typescript-eslint/parser': 5.50.0_4vsywjlpuriuw3tl5oq6zy5a64 array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 @@ -6379,7 +6325,7 @@ packages: doctrine: 2.1.0 eslint: 8.33.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4_ypqpzq5szckeh62pb722iz7nn4 + eslint-module-utils: 2.7.4_eslint@8.33.0 has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 @@ -6388,10 +6334,6 @@ packages: resolve: 1.22.1 semver: 6.3.0 tsconfig-paths: 3.14.1 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color dev: true /eslint-plugin-jsdoc/39.7.5_eslint@8.33.0: @@ -6755,8 +6697,6 @@ packages: type-is: 1.6.18 utils-merge: 1.0.1 vary: 1.1.2 - transitivePeerDependencies: - - supports-color dev: true /extend-shallow/2.0.1: @@ -6875,8 +6815,6 @@ packages: parseurl: 1.3.3 statuses: 2.0.1 unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color dev: true /find-cache-dir/3.3.2: @@ -6957,7 +6895,7 @@ packages: signal-exit: 3.0.7 dev: true - /fork-ts-checker-webpack-plugin/6.5.2_hhrrucqyg4eysmfpujvov2ym5u: + /fork-ts-checker-webpack-plugin/6.5.2_typescript@4.9.5+webpack@5.75.0: resolution: {integrity: sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==} engines: {node: '>=10', yarn: '>=1.0.0'} peerDependencies: @@ -7442,14 +7380,14 @@ packages: resolution: {integrity: sha512-eIkbX+8aAva5t6wvTMCxl90uKm5sXcjY20d+aEokHqu5I/MJECFbbkbnjUhYZVsKg1q2wuF9pnV9RrErHt5jyQ==} engines: {node: '>=14'} dependencies: - '@typescript-eslint/eslint-plugin': 5.50.0_go4drrxstycfikanvu45pi4vgq - '@typescript-eslint/parser': 5.50.0_4vsywjlpuriuw3tl5oq6zy5a64 + '@typescript-eslint/eslint-plugin': 5.50.0_33b838c6f29e0454280dad39d7a39534 + '@typescript-eslint/parser': 5.50.0_eslint@8.33.0+typescript@4.9.5 eslint: 8.33.0 eslint-config-prettier: 8.6.0_eslint@8.33.0 - eslint-config-standard: 17.0.0_xh3wrndcszbt2l7hdksdjqnjcq - eslint-config-standard-with-typescript: 30.0.0_frfgwa7fqjzszldru3sxumpviq - eslint-plugin-etc: 2.0.2_4vsywjlpuriuw3tl5oq6zy5a64 - eslint-plugin-import: 2.27.5_ufewo3pl5nnmz6lltvjrdi2hii + eslint-config-standard: 17.0.0_b9f768b46296433d2fe71aa434c1a914 + eslint-config-standard-with-typescript: 30.0.0_2c4a6b03e582732cac71a6e57a31f544 + eslint-plugin-etc: 2.0.2_eslint@8.33.0+typescript@4.9.5 + eslint-plugin-import: 2.27.5_eslint@8.33.0 eslint-plugin-jsdoc: 39.7.5_eslint@8.33.0 eslint-plugin-n: 15.6.1_eslint@8.33.0 eslint-plugin-no-only-tests: 3.1.0 @@ -7463,8 +7401,6 @@ packages: typescript: 4.9.5 transitivePeerDependencies: - enquirer - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - supports-color dev: true @@ -7472,14 +7408,14 @@ packages: resolution: {integrity: sha512-nDWeib3SxaHZRz0YhRkOnBDT5LAyMx6BXITO5xsocUJh4bSaqn7ha/h9Zlhw0WLtfxSVEXv96kjp/LQts12B9A==} engines: {node: '>=14'} dependencies: - '@typescript-eslint/eslint-plugin': 5.50.0_go4drrxstycfikanvu45pi4vgq - '@typescript-eslint/parser': 5.50.0_4vsywjlpuriuw3tl5oq6zy5a64 + '@typescript-eslint/eslint-plugin': 5.50.0_33b838c6f29e0454280dad39d7a39534 + '@typescript-eslint/parser': 5.50.0_eslint@8.33.0+typescript@4.9.5 eslint: 8.33.0 eslint-config-prettier: 8.6.0_eslint@8.33.0 - eslint-config-standard: 17.0.0_xh3wrndcszbt2l7hdksdjqnjcq - eslint-config-standard-with-typescript: 30.0.0_frfgwa7fqjzszldru3sxumpviq - eslint-plugin-etc: 2.0.2_4vsywjlpuriuw3tl5oq6zy5a64 - eslint-plugin-import: 2.27.5_ufewo3pl5nnmz6lltvjrdi2hii + eslint-config-standard: 17.0.0_b9f768b46296433d2fe71aa434c1a914 + eslint-config-standard-with-typescript: 30.0.0_2c4a6b03e582732cac71a6e57a31f544 + eslint-plugin-etc: 2.0.2_eslint@8.33.0+typescript@4.9.5 + eslint-plugin-import: 2.27.5_eslint@8.33.0 eslint-plugin-jsdoc: 39.7.5_eslint@8.33.0 eslint-plugin-n: 15.6.1_eslint@8.33.0 eslint-plugin-no-only-tests: 3.1.0 @@ -7493,8 +7429,6 @@ packages: typescript: 4.9.5 transitivePeerDependencies: - enquirer - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - supports-color dev: true @@ -8464,6 +8398,7 @@ packages: /jsonc-parser/3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: false /jsonfile/6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -8739,6 +8674,7 @@ packages: /lunr/2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + dev: false /magic-string/0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} @@ -8767,6 +8703,7 @@ packages: resolution: {integrity: sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==} engines: {node: '>= 12'} hasBin: true + dev: false /matcher/5.0.0: resolution: {integrity: sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw==} @@ -9026,6 +8963,7 @@ packages: engines: {node: '>=10'} dependencies: brace-expansion: 2.0.1 + dev: false /minimist/1.2.7: resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} @@ -9935,7 +9873,7 @@ packages: postcss-selector-parser: 6.0.11 dev: true - /postcss-loader/7.0.2_6jdsrmfenkuhhw3gx4zvjlznce: + /postcss-loader/7.0.2_postcss@8.4.21+webpack@5.75.0: resolution: {integrity: sha512-fUJzV/QH7NXUAqV8dWJ9Lg4aTkDCezpTS5HgJ2DvqznexTbSTxgi/dTECvTZ15BwKTtk8G/bqI/QTu2HPd3ZCg==} engines: {node: '>= 14.15.0'} peerDependencies: @@ -10502,15 +10440,9 @@ packages: strip-json-comments: 2.0.1 dev: true - /react-dev-utils/12.0.1_hhrrucqyg4eysmfpujvov2ym5u: + /react-dev-utils/12.0.1_typescript@4.9.5+webpack@5.75.0: resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} engines: {node: '>=14'} - peerDependencies: - typescript: '>=2.7' - webpack: '>=4' - peerDependenciesMeta: - typescript: - optional: true dependencies: '@babel/code-frame': 7.18.6 address: 1.2.2 @@ -10521,7 +10453,7 @@ packages: escape-string-regexp: 4.0.0 filesize: 8.0.7 find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 6.5.2_hhrrucqyg4eysmfpujvov2ym5u + fork-ts-checker-webpack-plugin: 6.5.2_typescript@4.9.5+webpack@5.75.0 global-modules: 2.0.0 globby: 11.1.0 gzip-size: 6.0.0 @@ -10536,12 +10468,11 @@ packages: shell-quote: 1.8.0 strip-ansi: 6.0.1 text-table: 0.2.0 - typescript: 4.9.5 - webpack: 5.75.0 transitivePeerDependencies: - eslint - - supports-color + - typescript - vue-template-compiler + - webpack dev: true /react-error-overlay/6.0.11: @@ -10557,11 +10488,6 @@ packages: peerDependencies: react: ^16.6.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true dependencies: '@babel/runtime': 7.20.13 invariant: 2.2.4 @@ -10574,15 +10500,12 @@ packages: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: true - /react-loadable-ssr-addon-v5-slorber/1.0.1_pwfl7zyferpbeh35vaepqxwaky: + /react-loadable-ssr-addon-v5-slorber/1.0.1_7d8abfe705245e121f7da808f85ec056: resolution: {integrity: sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==} engines: {node: '>=10.13.0'} peerDependencies: react-loadable: '*' webpack: '>=4.41.1 || 5.x' - peerDependenciesMeta: - react-loadable: - optional: true dependencies: '@babel/runtime': 7.20.13 react-loadable: /@docusaurus/react-loadable/5.5.2 @@ -10594,11 +10517,6 @@ packages: peerDependencies: react: '>=15' react-router: '>=5' - peerDependenciesMeta: - react: - optional: true - react-router: - optional: true dependencies: '@babel/runtime': 7.20.13 react-router: 5.3.4 @@ -10608,9 +10526,6 @@ packages: resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} peerDependencies: react: '>=15' - peerDependenciesMeta: - react: - optional: true dependencies: '@babel/runtime': 7.20.13 history: 4.10.1 @@ -10625,9 +10540,6 @@ packages: resolution: {integrity: sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==} peerDependencies: react: '>=15' - peerDependenciesMeta: - react: - optional: true dependencies: '@babel/runtime': 7.20.13 history: 4.10.1 @@ -11178,8 +11090,6 @@ packages: on-finished: 2.4.1 range-parser: 1.2.1 statuses: 2.0.1 - transitivePeerDependencies: - - supports-color dev: true /serialize-error/7.0.1: @@ -11225,8 +11135,6 @@ packages: http-errors: 1.6.3 mime-types: 2.1.35 parseurl: 1.3.3 - transitivePeerDependencies: - - supports-color dev: true /serve-static/1.15.0: @@ -11237,8 +11145,6 @@ packages: escape-html: 1.0.3 parseurl: 1.3.3 send: 0.18.0 - transitivePeerDependencies: - - supports-color dev: true /set-blocking/2.0.0: @@ -11311,6 +11217,7 @@ packages: jsonc-parser: 3.2.0 vscode-oniguruma: 1.7.0 vscode-textmate: 8.0.0 + dev: false /side-channel/1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} @@ -12026,7 +11933,7 @@ packages: /tslib/2.5.0: resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} - /tsutils-etc/1.4.1_dw2ve3pa3py4wrhanasku2jsqi: + /tsutils-etc/1.4.1_tsutils@3.21.0+typescript@4.9.5: resolution: {integrity: sha512-6UPYgc7OXcIW5tFxlsZF3OVSBvDInl/BkS3Xsu64YITXk7WrnWTVByKWPCThFDBp5gl5IGHOzGMdQuDCE7OL4g==} hasBin: true peerDependencies: @@ -12152,11 +12059,13 @@ packages: minimatch: 5.1.6 shiki: 0.12.1 typescript: 4.9.5 + dev: false /typescript/4.9.5: resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} hasBin: true + dev: true /uglify-js/3.17.4: resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} @@ -12349,7 +12258,7 @@ packages: dependencies: punycode: 2.3.0 - /url-loader/4.1.1_p5dl6emkcwslbw72e37w4ug7em: + /url-loader/4.1.1_file-loader@6.2.0+webpack@5.75.0: resolution: {integrity: sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==} engines: {node: '>= 10.13.0'} peerDependencies: @@ -12460,9 +12369,11 @@ packages: /vscode-oniguruma/1.7.0: resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} + dev: false /vscode-textmate/8.0.0: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} + dev: false /wait-on/6.0.1: resolution: {integrity: sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==}