diff --git a/packages/access-client/package.json b/packages/access-client/package.json index 39f48119a..1a42faec9 100644 --- a/packages/access-client/package.json +++ b/packages/access-client/package.json @@ -18,10 +18,6 @@ "lint": "tsc --build && eslint '**/*.{js,ts}' && prettier --check '**/*.{js,ts,yml,json}' --ignore-path ../../.gitignore", "build": "tsc --build", "check": "tsc --build", - "test": "pnpm -r run build && npm run test:node && npm run test:browser", - "test:node": "mocha 'test/**/!(*.browser).test.js' -n experimental-vm-modules -n no-warnings", - "test:browser": "playwright-test 'test/**/!(*.node).test.js'", - "testw": "watch 'pnpm test' src test --interval 1", "rc": "npm version prerelease --preid rc" }, "exports": { @@ -97,41 +93,11 @@ "dist/src/**/*.d.ts.map" ], "dependencies": { - "@ipld/car": "^5.1.1", - "@ipld/dag-ucan": "^3.4.0", - "@ucanto/client": "^9.0.0", - "@ucanto/core": "^9.0.0", - "@ucanto/interface": "^9.0.0", - "@ucanto/principal": "^9.0.0", - "@ucanto/transport": "^9.0.0", - "@ucanto/validator": "^9.0.0", - "@web3-storage/capabilities": "workspace:^", - "@web3-storage/did-mailto": "workspace:^", - "bigint-mod-arith": "^3.1.2", - "conf": "11.0.2", - "multiformats": "^12.1.2", - "one-webcrypto": "git://github.com/web3-storage/one-webcrypto", - "p-defer": "^4.0.0", - "type-fest": "^3.3.0", - "uint8arrays": "^4.0.6", - "@scure/bip39": "^1.2.1" + "@web3-storage/w3up-client": "workspace:^" }, "devDependencies": { "@web3-storage/eslint-config-w3up": "workspace:^", - "@types/assert": "^1.5.6", - "@types/inquirer": "^9.0.4", - "@types/mocha": "^10.0.1", - "@types/node": "^20.8.4", - "@types/sinon": "^10.0.19", - "@types/varint": "^6.0.1", - "@types/ws": "^8.5.4", - "@ucanto/server": "^9.0.1", - "assert": "^2.0.0", - "mocha": "^10.2.0", - "playwright-test": "^12.3.4", - "sinon": "^15.0.3", - "typescript": "5.2.2", - "watch": "^1.0.2" + "typescript": "5.2.2" }, "eslintConfig": { "extends": [ diff --git a/packages/access-client/readme.md b/packages/access-client/readme.md index f10d07fc7..66582a476 100644 --- a/packages/access-client/readme.md +++ b/packages/access-client/readme.md @@ -1,171 +1,3 @@ -


web3.storage

-

The access client for https://web3.storage

+# ⚠️ Deprecated -## About - -The `@web3-storage/access` package provides an API for creating and managing "agents," which are software entities that control private signing keys and can invoke capabilities on behalf of a user (or another agent). - -Agents are used to invoke capabilities provided by the w3up service layer, using the [ucanto](https://github.com/web3-storage/ucanto) RPC framework. Agents are created locally on an end-user's device, and users are encouraged to create new agents for each device (or browser) that they want to use, rather than sharing agent keys between devices. - -An Agent can create "spaces," which are namespaces for content stored on the w3up platform. Each space has its own keypair, the public half of which is used to form a `did:key:` URI that uniquely identifies the space. The space's private key is used to delegate capabilities to a primary agent, which then issues ucanto requests related to the space. - -Although agents (and spaces) are created locally by generating keypairs, the w3up services will only act upon spaces that have been registered with the w3up access service. By default, a newly-created agent will be configured to use the production access service for remote operations, including registration. - -Please note that the `@web3-storage/access` package is a fairly "low level" component of the w3up JavaScript stack, and most users will be better served by [`@web3-storage/w3up-client`](https://github.com/web3-storage/w3up-client), which combines this package with a client for the upload and storage service and presents a simpler API. - -## Install - -Install the package: - -```bash -npm install @web3-storage/access -``` - -## Usage - -[API Reference](https://web3-storage.github.io/w3protocol/modules/_web3_storage_access.html) - -### Agent creation - -To create an agent, you must first create a `Store`, which the agent will use to store and manage persistent state, including private keys. - -If you're running in a web browser, use [`StoreIndexedDB`](https://web3-storage.github.io/w3protocol/classes/_web3_storage_access.StoreIndexedDB.html), which uses IndexedDB to store non-extractable [`CryptoKey`](https://www.w3.org/TR/WebCryptoAPI/#dfn-CryptoKey) objects. This prevents the private key material from ever being exposed to the JavaScript environment. - -Agents in a browser use RSA keys, which can be generated using the async `generate` function from `@ucanto/principal/rsa`. - -```js -import { Agent } from '@web3-storage/access/agent' -import { StoreIndexedDB } from '@web3-storage/access/stores/store-indexeddb' -import { generate } from '@ucanto/principal/rsa' - -async function createAgent() { - const store = await StoreIndexedDB.open('my-db-name') - - // if agent data already exists in the store, use it to create an Agent. - const data = await store.load() - if (data) { - return Agent.from(data, { store }) - } - - // otherwise, generate a new RSA signing key to act as the Agent's principal - // and create a new Agent, passing in the store so the Agent can persist its state - const principal = await generate() - const agentData = { - meta: { name: 'my-browser-agent' }, - principal - } - return Agent.create(agentData, { store }) -} -``` - -On node.js, use [`StoreConf`](https://web3-storage.github.io/w3protocol/classes/_web3_storage_access.StoreConf.html), which uses the [`conf` package](https://www.npmjs.com/package/conf) to store keys and metadata in the user's platform-specific default configuration location (usually in their home directory). - -Agents on node should use Ed25519 keys: - -```js -import { Agent } from '@web3-storage/access/agent' -import { StoreConf } from '@web3-storage/access/stores/store-conf' -import { generate } from '@ucanto/principal/ed25519' - -async function createAgent() { - const store = new StoreConf({ profile: 'my-w3up-app' }) - if (!(await store.exists())) { - await store.init({}) - } - - // if agent data already exists in the store, use it to create an Agent. - const data = await store.load() - if (data) { - return Agent.from(data, { store }) - } - - // otherwise, generate a new Ed25519 signing key to act as the Agent's principal - // and create a new Agent, passing in the store so the Agent can persist its state - const principal = await generate() - const agentData = { - meta: { name: 'my-nodejs-agent' }, - principal - } - return Agent.create(agentData, { store }) -} - -``` - -See the [`AgentCreateOptions` reference](https://web3-storage.github.io/w3protocol/interfaces/_web3_storage_access._internal_.AgentCreateOptions.html) if you want to configure the agent to use a non-production service connection. - -### Space creation - -A newly-created agent does not have access to any spaces, and is thus unable to store data using the w3up platform. - -To create a new space, use [`agent.createSpace`](https://web3-storage.github.io/w3protocol/classes/_web3_storage_access.Agent.html#createSpace), optionally passing in a human-readable name. - -This will create a new signing keypair for the space and use it to issue a non-expiring delegation for all space-related capabilities to the agent, which will persist the delegation in its Store for future use. - -The `createSpace` method returns an object describing the space: - -```ts -interface CreateSpaceResult { - /** The Space's Decentralized Identity Document (DID) */ - did: string, - - /** - * Metadata for the Space, including optional friendly `name` and an `isRegistered` flag. - * Persisted locally in the Agent's Store. - */ - meta: Record, - - /** - * Cryptographic proof of the delegation from Space to Agent. - * Persisted locally in the Agent's Store. - */ - proof: Ucanto.Delegation -} -``` - -### Managing the current space - -An agent can create multiple spaces and may also be issued delegations that allow it to manage spaces created by other agents. The agent's `spaces` property is a `Map` keyed by space DID, whose values are the metadata associated with each space. - -Agents may also have a "current" space, which is used as the default space for storage operations if none is specified. You can retrieve the DID of the current space with [`agent.currentSpace`](https://web3-storage.github.io/w3protocol/classes/_web3_storage_access.Agent.html#currentSpace). If you also want the metadata and proofs associated with the space, use [`agent.currentSpaceWithMeta`](https://web3-storage.github.io/w3protocol/classes/_web3_storage_access.Agent.html#currentSpaceWithMeta). - -To set the current space, use [`agent.setCurrentSpace`](https://web3-storage.github.io/w3protocol/classes/_web3_storage_access.Agent.html#setCurrentSpace). Note that this must be done explicitly; creating an agent's first space does not automatically set it as the current space. - -### Space registration - -A newly-created space must be registered with the w3up access service before it can be used as a storage location. - -To register a space, use [`agent.registerSpace`](https://web3-storage.github.io/w3protocol/classes/_web3_storage_access.Agent.html#registerSpace), which takes an email address parameter and registers the [current space](#managing-the-current-space) with the access service. - -Calling `registerSpace` will cause the access service to send a confirmation email to the provided email address. When the activation link in the email is clicked, the service will send the agent a delegation via a WebSocket connection that grants access to the services included in w3up's free tier. The `registerSpace` method returns a `Promise` that resolves once the registration process is complete. Make sure to wrap calls to `registerSpace` in a `try/catch` block, as registration will fail if the user does not confirm the email (or if network issues arise, etc.). - -### Delegating to another agent - -The agent's [`delegate` method](https://web3-storage.github.io/w3protocol/classes/_web3_storage_access.Agent.html#delegate) allows you to delegate capabilities to another agent. - -```js -const delegation = await agent.delegate({ - audience: 'did:key:kAgentToDelegateTo', - abilities: [ - { - can: 'space/info', - with: agent.currentSpace() - } - ] -}) -``` - -Note that the receiving agent will need to [import the delegation](#importing-delegations-from-another-agent) before they will be able to invoke the delegated capabilities. - -### Importing delegations from another agent - -The [`addProof` method](https://web3-storage.github.io/w3protocol/classes/_web3_storage_access.Agent.html#addProof) takes in a ucanto `Delegation` and adds it to the agent's state Store. The proof of delegation can be retrieved using the agent's [`proofs` method](https://web3-storage.github.io/w3protocol/classes/_web3_storage_access.Agent.html#proofs). - -The [`importSpaceFromDelegation` method](https://web3-storage.github.io/w3protocol/classes/_web3_storage_access.Agent.html#importSpaceFromDelegation) also accepts a ucanto `Delegation`, but it is tailored for "full delegation" of all space-related capabilities. The delegated ability must be `*`, which is the "top" ability that can derive all abilities for the Space's DID. Use `importSpaceFromDelegation` in preference to `addProofs` when importing a full `*` delegation for a space, as it also adds metadata about the imported space to the Agent's persistent store and adds the space to the agent's set of authorized spaces. - -## Contributing - -Feel free to join in. All welcome. Please [open an issue](https://github.com/web3-storage/w3protocol/issues)! - -## License - -Dual-licensed under [MIT + Apache 2.0](https://github.com/web3-storage/w3protocol/blob/main/license.md) +Use `@web3-storage/w3up-client` instead. diff --git a/packages/access-client/src/access.js b/packages/access-client/src/access.js index 9d288875e..861c8e0fc 100644 --- a/packages/access-client/src/access.js +++ b/packages/access-client/src/access.js @@ -1,344 +1 @@ -import * as Access from '@web3-storage/capabilities/access' -import * as API from './types.js' -import { Failure, fail, DID } from '@ucanto/core' -import { Agent, importAuthorization } from './agent.js' -import { bytesToDelegations } from './encoding.js' - -/** - * Takes array of delegations and propagates them to their respective audiences - * through a given space (or the current space if none is provided). - * - * Returns error result if agent has no current space and no space was provided. - * Also returns error result if invocation fails. - * - * @param {Agent} agent - Agent connected to the w3up service. - * @param {object} input - * @param {API.Delegation[]} input.delegations - Delegations to propagate. - * @param {API.SpaceDID} [input.space] - Space to propagate through. - * @param {API.Delegation[]} [input.proofs] - Optional set of proofs to be - * included in the invocation. - */ -export const delegate = async ( - agent, - { delegations, proofs = [], space = agent.currentSpace() } -) => { - if (!space) { - return fail('Space must be specified') - } - - const entries = Object.values(delegations).map((proof) => [ - proof.cid.toString(), - proof.cid, - ]) - - const { out } = await agent.invokeAndExecute(Access.delegate, { - with: space, - nb: { - delegations: Object.fromEntries(entries), - }, - // must be embedded here because it's referenced by cid in .nb.delegations - proofs: [...delegations, ...proofs], - }) - - return out -} - -/** - * Requests specified `access` level from specified `account`. It invokes - * `access/authorize` capability, if invocation succeeds it will return a - * `PendingAccessRequest` object that can be used to poll for the requested - * delegation through `access/claim` capability. - * - * @param {API.Agent} agent - * @param {object} input - * @param {API.AccountDID} input.account - Account from which access is requested. - * @param {API.ProviderDID} [input.provider] - Provider that will receive the invocation. - * @param {API.DID} [input.audience] - Principal requesting an access. - * @param {API.Access} [input.access] - Access been requested. - * @returns {Promise>} - */ -export const request = async ( - agent, - { - account, - provider = /** @type {API.ProviderDID} */ (agent.connection.id.did()), - audience: audience = agent.did(), - access = spaceAccess, - } -) => { - // Request access from the account. - const { out: result } = await agent.invokeAndExecute(Access.authorize, { - audience: DID.parse(provider), - with: audience, - nb: { - iss: account, - // New ucan spec moved to recap style layout for capabilities and new - // `access/request` will use similar format as opposed to legacy one, - // in the meantime we translate new format to legacy format here. - att: [...toCapabilities(access)], - }, - }) - - return result.error - ? result - : { - ok: new PendingAccessRequest({ - ...result.ok, - agent, - audience, - provider, - }), - } -} - -/** - * Claims access that has been delegated to the given audience, which by - * default is the agent's DID. - * - * @param {API.Agent} agent - * @param {object} input - * @param {API.DID} [input.audience] - Principal requesting an access. - * @param {API.ProviderDID} [input.provider] - Provider handling the invocation. - * @returns {Promise>} - */ -export const claim = async ( - agent, - { - provider = /** @type {API.ProviderDID} */ (agent.connection.id.did()), - audience = agent.did(), - } = {} -) => { - const { out: result } = await agent.invokeAndExecute(Access.claim, { - audience: DID.parse(provider), - with: audience, - }) - - if (result.error) { - return result - } else { - const delegations = Object.values(result.ok.delegations) - const proofs = delegations.flatMap((proof) => bytesToDelegations(proof)) - return { ok: new GrantedAccess({ agent, provider, audience, proofs }) } - } -} - -/** - * Represents a pending access request. It can be used to poll for the requested - * delegation. - */ -class PendingAccessRequest { - /** - * @typedef {object} PendingAccessRequestModel - * @property {API.Agent} agent - Agent handling interaction. - * @property {API.DID} audience - Principal requesting an access. - * @property {API.ProviderDID} provider - Provider handling request. - * @property {API.UTCUnixTimestamp} expiration - Seconds in UTC. - * @property {API.Link} request - Link to the `access/authorize` invocation. - * - * @param {PendingAccessRequestModel} model - */ - constructor(model) { - this.model = model - } - - get agent() { - return this.model.agent - } - get audience() { - return this.model.audience - } - get expiration() { - return new Date(this.model.expiration * 1000) - } - - get request() { - return this.model.request - } - - get provider() { - return this.model.provider - } - - /** - * Low level method and most likely you want to use `.claim` instead. This method will poll - * fetch delegations **just once** and will return proofs matching to this request. Please note - * that there may not be any matches in which case result will be `{ ok: [] }`. - * - * If you do want to continuously poll until request is approved or expired, you should use - * `.claim` method instead. - * - * @returns {Promise>} - */ - async poll() { - const { agent, audience, provider, expiration } = this.model - const timeout = expiration * 1000 - Date.now() - if (timeout <= 0) { - return { error: new RequestExpired(this.model) } - } else { - const result = await claim(agent, { audience, provider }) - return result.error - ? result - : { - ok: result.ok.proofs.filter((proof) => - isRequestedAccess(proof, this.model) - ), - } - } - } - - /** - * Continuously polls delegations until this request is approved or expired. Returns - * a `GrantedAccess` object (view over the delegations) that can be used in the - * invocations or can be saved in the agent (store) using `.save()` method. - * - * @param {object} options - * @param {number} [options.interval] - * @param {AbortSignal} [options.signal] - * @returns {Promise>} - */ - async claim({ signal, interval = 250 } = {}) { - while (signal?.aborted !== true) { - const result = await this.poll() - // If polling failed, return the error. - if (result.error) { - return result - } - // If we got some matching proofs, return them. - else if (result.ok.length > 0) { - return { - ok: new GrantedAccess({ - agent: this.agent, - provider: this.provider, - audience: this.audience, - proofs: result.ok, - }), - } - } - - await new Promise((resolve) => setTimeout(resolve, interval)) - } - - return { - error: Object.assign(new Error('Aborted'), { reason: signal.reason }), - } - } -} - -/** - * Error returned when pending access request expires. - */ -class RequestExpired extends Failure { - /** - * @param {PendingAccessRequestModel} model - */ - constructor(model) { - super() - this.model = model - } - - get name() { - return 'RequestExpired' - } - - get request() { - return this.model.request - } - get expiredAt() { - return new Date(this.model.expiration * 1000) - } - - describe() { - return `Access request expired at ${this.expiredAt} for ${this.request} request.` - } -} - -/** - * View over the UCAN Delegations that grant access to a specific principal. - */ -class GrantedAccess { - /** - * @typedef {object} GrantedAccessModel - * @property {API.Agent} agent - Agent that processed the request. - * @property {API.DID} audience - Principal access was granted to. - * @property {API.Delegation[]} proofs - Delegations that grant access. - * @property {API.ProviderDID} provider - Provider that handled the request. - * - * @param {GrantedAccessModel} model - */ - constructor(model) { - this.model = model - } - get proofs() { - return this.model.proofs - } - get provider() { - return this.model.provider - } - get authority() { - return this.model.audience - } - - /** - * Saves access into the agents proofs store so that it can be retained - * between sessions. - * - * @param {object} input - * @param {API.Agent} [input.agent] - */ - save({ agent = this.model.agent } = {}) { - return importAuthorization(agent, this) - } -} - -/** - * Checks if the given delegation is caused by the passed `request` for access. - * - * @param {API.Delegation} delegation - * @param {object} selector - * @param {API.Link} selector.request - * @returns - */ -const isRequestedAccess = (delegation, { request }) => - // `access/confirm` handler adds facts to the delegation issued by the account - // so that principal requesting access can identify correct delegation when - // access is granted. - delegation.facts.some((fact) => `${fact['access/request']}` === `${request}`) - -/** - * Maps access object that uses UCAN 0.10 capabilities format as opposed - * to legacy UCAN 0.9 format used by w3up which predates new format. - * - * @param {API.Access} access - * @returns {{ can: API.Ability }[]} - */ -export const toCapabilities = (access) => { - const abilities = [] - const entries = /** @type {[API.Ability, API.Unit][]} */ ( - Object.entries(access) - ) - - for (const [can, details] of entries) { - if (details) { - abilities.push({ can }) - } - } - return abilities -} - -/** - * Set of capabilities required by the agent to manage a space. - */ -export const spaceAccess = { - 'space/*': {}, - 'store/*': {}, - 'upload/*': {}, - 'access/*': {}, - 'filecoin/*': {}, -} - -/** - * Set of capabilities required for by the agent to manage an account. - */ -export const accountAccess = { - '*': {}, -} +export * from '@web3-storage/w3up-client/capability/access' diff --git a/packages/access-client/src/agent.js b/packages/access-client/src/agent.js index fca9a67bf..1d98ce004 100644 --- a/packages/access-client/src/agent.js +++ b/packages/access-client/src/agent.js @@ -1,658 +1 @@ -import * as Client from '@ucanto/client' -import * as CAR from '@ucanto/transport/car' -import * as HTTP from '@ucanto/transport/http' -import * as ucanto from '@ucanto/core' -import * as Capabilities from '@web3-storage/capabilities/space' -import { attest } from '@web3-storage/capabilities/ucan' -import * as Access from './access.js' -import * as Space from './space.js' - -import { invoke, delegate, DID, Delegation, Schema } from '@ucanto/core' -import { isExpired, isTooEarly, canDelegateCapability } from './delegations.js' -import { AgentData, getSessionProofs } from './agent-data.js' -import { UCAN } from '@web3-storage/capabilities' - -import * as API from './types.js' - -export * from './types.js' -export * from './delegations.js' -export { AgentData, Access, Space, Delegation, Schema } -export * from './agent-use-cases.js' - -const HOST = 'https://up.web3.storage' -const PRINCIPAL = DID.parse('did:web:web3.storage') - -/** - * Keeps track of AgentData for all Agents constructed. - * Used by addSpacesFromDelegations - so it can only accept Agent as param, but - * still mutate corresponding AgentData - * - * @deprecated - remove this when deprecated addSpacesFromDelegations is removed - */ -/** @type {WeakMap>, AgentData>} */ -const agentToData = new WeakMap() - -/** - * @typedef {API.Service} Service - * @typedef {API.Receipt} Receipt - */ - -/** - * Creates a Ucanto connection for the w3access API - * - * Usage: - * - * ```js - * import { connection } from '@web3-storage/access/agent' - * ``` - * - * @template {API.DID} T - DID method - * @template {Record} [S=Service] - * @param {object} [options] - * @param {API.Principal} [options.principal] - w3access API Principal - * @param {URL} [options.url] - w3access API URL - * @param {API.Transport.Channel} [options.channel] - Ucanto channel to use - * @param {typeof fetch} [options.fetch] - Fetch implementation to use - * @returns {API.ConnectionView} - */ -export function connection(options = {}) { - return Client.connect({ - id: options.principal ?? PRINCIPAL, - codec: CAR.outbound, - channel: - options.channel ?? - HTTP.open({ - url: options.url ?? new URL(HOST), - method: 'POST', - fetch: options.fetch ?? globalThis.fetch.bind(globalThis), - }), - }) -} - -/** - * Agent - * - * Usage: - * - * ```js - * import { Agent } from '@web3-storage/access/agent' - * ``` - * - * @template {Record} [S=Service] - */ -export class Agent { - /** @type {import('./agent-data.js').AgentData} */ - #data - - /** - * @param {import('./agent-data.js').AgentData} data - Agent data - * @param {import('./types.js').AgentOptions} [options] - */ - constructor(data, options = {}) { - /** @type { Client.Channel & { url?: URL } | undefined } */ - const channel = options.connection?.channel - this.url = options.url ?? channel?.url ?? new URL(HOST) - this.connection = - options.connection ?? - connection({ - principal: options.servicePrincipal, - url: this.url, - }) - this.#data = data - agentToData.set(this, this.#data) - } - - /** - * Create a new Agent instance, optionally with the passed initialization data. - * - * @template {Record} [R=Service] - * @param {Partial} [init] - * @param {import('./types.js').AgentOptions & import('./types.js').AgentDataOptions} [options] - */ - static async create(init, options = {}) { - const data = await AgentData.create(init, options) - return new Agent(data, options) - } - - /** - * Instantiate an Agent from pre-exported agent data. - * - * @template {Record} [R=Service] - * @param {import('./types.js').AgentDataExport} raw - * @param {import('./types.js').AgentOptions & import('./types.js').AgentDataOptions} [options] - */ - static from(raw, options = {}) { - const data = AgentData.fromExport(raw, options) - return new Agent(data, options) - } - - get issuer() { - return this.#data.principal - } - - get meta() { - return this.#data.meta - } - - get spaces() { - return this.#data.spaces - } - - did() { - return this.#data.principal.did() - } - - /** - * Add a proof to the agent store. - * - * @param {API.Delegation} delegation - */ - async addProof(delegation) { - return await this.addProofs([delegation]) - } - - /** - * Adds set of proofs to the agent store. - * - * @param {Iterable} delegations - */ - async addProofs(delegations) { - for (const proof of delegations) { - await this.#data.addDelegation(proof, { audience: this.meta }) - } - await this.removeExpiredDelegations() - - return {} - } - - /** - * Query the delegations store for all the delegations matching the capabilities provided. - * - * @param {API.CapabilityQuery[]} [caps] - */ - #delegations(caps) { - const _caps = new Set(caps) - /** @type {Array<{ delegation: API.Delegation, meta: API.DelegationMeta }>} */ - const values = [] - for (const [, value] of this.#data.delegations) { - // check expiration - if ( - !isExpired(value.delegation) && // check if delegation can be used - !isTooEarly(value.delegation) - ) { - // check if we need to filter for caps - if (Array.isArray(caps) && caps.length > 0) { - for (const cap of _caps) { - if (canDelegateCapability(value.delegation, cap)) { - values.push(value) - } - } - } else { - values.push(value) - } - } - } - return values - } - - /** - * Clean up any expired delegations. - */ - async removeExpiredDelegations() { - for (const [, value] of this.#data.delegations) { - if (isExpired(value.delegation)) { - await this.#data.removeDelegation(value.delegation.cid) - } - } - } - - /** - * Revoke a delegation by CID. - * - * If the delegation was issued by this agent (and therefore is stored in the - * delegation store) you can just pass the CID. If not, or if the current agent's - * delegation store no longer contains the delegation, you MUST pass a chain of - * proofs that proves your authority to revoke this delegation as `options.proofs`. - * - * @param {API.UCANLink} delegationCID - * @param {object} [options] - * @param {API.Delegation[]} [options.proofs] - */ - async revoke(delegationCID, options = {}) { - const additionalProofs = options.proofs ?? [] - // look for the identified delegation in the delegation store and the passed proofs - const delegation = [...this.delegations(), ...additionalProofs].find( - (delegation) => delegation.cid.equals(delegationCID) - ) - if (!delegation) { - return { - error: new Error( - `could not find delegation ${delegationCID.toString()} - please include the delegation in options.proofs` - ), - } - } - const receipt = await this.invokeAndExecute(UCAN.revoke, { - // per https://github.com/web3-storage/w3up/blob/main/packages/capabilities/src/ucan.js#L38C6-L38C6 the resource here should be - // the current issuer - using the space DID here works for simple cases but falls apart when a delegee tries to revoke a delegation - // they have re-delegated, since they don't have "ucan/revoke" capabilities on the space - with: this.issuer.did(), - nb: { - ucan: delegation.cid, - }, - proofs: [delegation, ...additionalProofs], - }) - return receipt.out - } - - /** - * Get all the proofs matching the capabilities. - * - * Proofs are delegations with an audience matching agent DID, or with an - * audience matching the session DID. - * - * Proof of session will also be included in the returned proofs if any - * proofs matching the passed capabilities require it. - * - * @param {API.CapabilityQuery[]} [caps] - Capabilities to filter by. Empty or undefined caps with return all the proofs. - * @param {object} [options] - * @param {API.DID} [options.sessionProofIssuer] - only include session proofs for this issuer - */ - proofs(caps, options) { - const authorizations = [] - for (const { delegation } of this.#delegations(caps)) { - if (delegation.audience.did() === this.issuer.did()) { - authorizations.push(delegation) - } - } - - // now let's add any session proofs that refer to those authorizations - const sessions = getSessionProofs(this.#data) - for (const proof of authorizations) { - const proofsByIssuer = sessions[proof.asCID.toString()] ?? {} - const sessionProofs = options?.sessionProofIssuer - ? proofsByIssuer[options.sessionProofIssuer] ?? [] - : Object.values(proofsByIssuer).flat() - if (sessionProofs.length) { - authorizations.push(...sessionProofs) - } - } - return authorizations - } - - /** - * Get delegations created by the agent for others. - * - * @param {API.CapabilityQuery[]} [caps] - Capabilities to filter by. Empty or undefined caps with return all the delegations. - */ - delegations(caps) { - const arr = [] - - for (const { delegation } of this.delegationsWithMeta(caps)) { - arr.push(delegation) - } - - return arr - } - - /** - * Get delegations created by the agent for others and their metadata. - * - * @param {API.CapabilityQuery[]} [caps] - Capabilities to filter by. Empty or undefined caps with return all the delegations. - */ - delegationsWithMeta(caps) { - const arr = [] - - for (const value of this.#delegations(caps)) { - const { delegation } = value - const isSession = delegation.capabilities.some( - (c) => c.can === attest.can - ) - if (!isSession && delegation.audience.did() !== this.issuer.did()) { - arr.push(value) - } - } - - return arr - } - - /** - * Creates a space signer and a delegation to the agent - * - * @param {string} name - */ - async createSpace(name) { - return await Space.generate({ name }) - } - - /** - * Import a space from a delegation. - * - * @param {API.Delegation} delegation - * @param {object} options - * @param {string} [options.name] - */ - async importSpaceFromDelegation(delegation, { name = '' } = {}) { - const space = - name === '' - ? Space.fromDelegation(delegation) - : Space.fromDelegation(delegation).withName(name) - - if (space.name === '') { - throw new Error( - 'Space has no name, please pass a `name` option to specify it' - ) - } - - this.#data.spaces.set(space.did(), { ...space.meta, name: space.name }) - - await this.addProof(space.delegation) - - // if we do not have a current space, make this one current - if (!this.currentSpace()) { - await this.setCurrentSpace(space.did()) - } - - return space - } - - /** - * Sets the current selected space - * - * Other methods will default to use the current space if no resource is defined - * - * @param {API.SpaceDID} space - */ - async setCurrentSpace(space) { - if (!this.#data.spaces.has(space)) { - throw new Error(`Agent has no proofs for ${space}.`) - } - - await this.#data.setCurrentSpace(space) - - return space - } - - /** - * Get current space DID - */ - currentSpace() { - return this.#data.currentSpace - } - - /** - * Get current space DID, proofs and abilities - */ - currentSpaceWithMeta() { - if (!this.#data.currentSpace) { - return - } - - const proofs = this.proofs([ - { - can: 'space/info', - with: this.#data.currentSpace, - }, - ]) - - const caps = new Set() - for (const p of proofs) { - for (const cap of p.capabilities) { - caps.add(cap.can) - } - } - - return { - did: this.#data.currentSpace, - proofs, - capabilities: [...caps], - meta: this.#data.spaces.get(this.#data.currentSpace), - } - } - - /** - * - * @param {import('./types.js').DelegationOptions} options - */ - async delegate(options) { - const space = this.currentSpaceWithMeta() - if (!space) { - throw new Error('no space selected.') - } - - const caps = /** @type {API.Capabilities} */ ( - options.abilities.map((a) => { - return { - with: space.did, - can: a, - } - }) - ) - - // Verify agent can provide proofs for each requested capability - for (const cap of caps) { - if (!this.proofs([cap]).length) { - throw new Error( - `cannot delegate capability ${cap.can} with ${cap.with}` - ) - } - } - - const delegation = await delegate({ - issuer: this.issuer, - capabilities: caps, - proofs: this.proofs(caps), - facts: [{ space: space.meta ?? {} }], - ...options, - }) - - await this.#data.addDelegation(delegation, { - audience: options.audienceMeta, - }) - await this.removeExpiredDelegations() - - return delegation - } - - /** - * Invoke and execute the given capability on the Access service connection - * - * ```js - * - * await agent.invokeAndExecute(Space.recover, { - * nb: { - * identity: 'mailto: email@gmail.com', - * }, - * }) - * - * // sugar for - * const recoverInvocation = await agent.invoke(Space.recover, { - * nb: { - * identity: 'mailto: email@gmail.com', - * }, - * }) - * - * await recoverInvocation.execute(agent.connection) - * ``` - * - * @template {API.Ability} A - * @template {API.URI} R - * @template {API.Caveats} C - * @param {API.TheCapabilityParser>} cap - * @param {API.InvokeOptions>>} options - * @returns {Promise, S>>} - */ - async invokeAndExecute(cap, options) { - const inv = await this.invoke(cap, options) - const out = inv.execute(/** @type {*} */ (this.connection)) - return /** @type {*} */ (out) - } - - /** - * Execute invocations on the agent's connection - * - * @example - * ```js - * const i1 = await agent.invoke(Space.info, {}) - * const i2 = await agent.invoke(Space.recover, { - * nb: { - * identity: 'mailto:hello@web3.storage', - * }, - * }) - * - * const results = await agent.execute2(i1, i2) - * - * ``` - * @template {API.Capability} C - * @template {API.Tuple>} I - * @param {I} invocations - */ - execute(...invocations) { - return this.connection.execute(...invocations) - } - - /** - * Creates an invocation for the given capability with Agent's proofs, service, issuer and space. - * - * @example - * ```js - * const recoverInvocation = await agent.invoke(Space.recover, { - * nb: { - * identity: 'mailto: email@gmail.com', - * }, - * }) - * - * await recoverInvocation.execute(agent.connection) - * // or - * await agent.execute(recoverInvocation) - * ``` - * - * @template {API.Ability} A - * @template {API.URI} R - * @template {API.TheCapabilityParser>} CAP - * @template {API.Caveats} [C={}] - * @param {CAP} cap - * @param {import('./types.js').InvokeOptions} options - */ - async invoke(cap, options) { - const audience = options.audience || this.connection.id - - const space = options.with || this.currentSpace() - if (!space) { - throw new Error( - 'No space or resource selected, you need pass a resource.' - ) - } - - const proofs = [ - ...(options.proofs || []), - ...this.proofs( - [ - { - with: space, - can: cap.can, - }, - ], - { sessionProofIssuer: audience.did() } - ), - ] - - if (proofs.length === 0 && options.with !== this.did()) { - throw new Error( - `no proofs available for resource ${space} and ability ${cap.can}` - ) - } - const inv = invoke({ - ...options, - audience, - // @ts-ignore - capability: cap.create({ - with: space, - nb: options.nb, - }), - issuer: this.issuer, - proofs: [...proofs], - }) - - return /** @type {API.IssuedInvocationView>} */ ( - inv - ) - } - - /** - * Get Space information from Access service - * - * @param {API.URI<"did:">} [space] - */ - async getSpaceInfo(space) { - const _space = space || this.currentSpace() - if (!_space) { - throw new Error('No space selected, you need pass a resource.') - } - const inv = await this.invokeAndExecute(Capabilities.info, { - with: _space, - }) - - if (inv.out.error) { - throw inv.out.error - } - - return /** @type {import('./types.js').SpaceInfoResult} */ (inv.out.ok) - } -} - -/** - * Given a list of delegations, add to agent data spaces list. - * - * @deprecated - trying to remove explicit space tracking from Agent/AgentData - * in favor of functions that derive the space set from access.delegations - * - * @template {Record} [S=Service] - * @param {Agent} agent - * @param {API.Delegation[]} delegations - */ -export async function addSpacesFromDelegations(agent, delegations) { - const data = agentToData.get(agent) - if (!data) { - throw Object.assign(new Error(`cannot determine AgentData for Agent`), { - agent: agent, - }) - } - - for (const delegation of delegations) { - // We only consider delegations to this agent as those are only spaces that - // this agent will be able to interact with. - if (delegation.audience.did() === agent.did()) { - // TODO: we need a more robust way to determine which spaces a user has access to - // it may or may not involve look at delegations - const allows = ucanto.Delegation.allows(delegation) - - for (const [did, value] of Object.entries(allows)) { - // If we discovered a delegation to any DID, we add it to the spaces list. - if (did.startsWith('did:key') && Object.keys(value).length > 0) { - data.addSpace(/** @type {API.DID} */ (did), { - name: '', - }) - } - } - } - } -} - -/** - * Stores given delegations in the agent's data store and adds discovered spaces - * to the agent's space list. - * - * @param {Agent} agent - * @param {object} authorization - * @param {API.Delegation[]} authorization.proofs - * @returns {Promise>} - */ -export const importAuthorization = async (agent, { proofs }) => { - try { - await agent.addProofs(proofs) - await addSpacesFromDelegations(agent, proofs) - return { ok: {} } - } catch (error) { - return /** @type {{error:Error}} */ ({ error }) - } -} +export * from '@web3-storage/w3up-client/agent' diff --git a/packages/access-client/src/drivers/conf.js b/packages/access-client/src/drivers/conf.js index ddf27bc10..bbcc1aa7b 100644 --- a/packages/access-client/src/drivers/conf.js +++ b/packages/access-client/src/drivers/conf.js @@ -1,68 +1 @@ -import Conf from 'conf' -import * as JSON from '../utils/json.js' - -/** - * @template T - * @typedef {import('./types.js').Driver} Driver - */ - -/** - * Driver implementation with "[conf](https://github.com/sindresorhus/conf)" - * - * Usage: - * - * ```js - * import { ConfDriver } from '@web3-storage/access/drivers/conf' - * ``` - * - * @template {Record} T - * @implements {Driver} - */ -export class ConfDriver { - /** - * @type {Conf} - */ - #config - - /** - * @param {{ profile: string }} opts - */ - constructor(opts) { - this.#config = new Conf({ - projectName: 'w3access', - projectSuffix: '', - configName: opts.profile, - serialize: (v) => JSON.stringify(v), - deserialize: (v) => JSON.parse(v), - }) - this.path = this.#config.path - } - - async open() {} - - async close() {} - - async reset() { - this.#config.clear() - } - - /** @param {T} data */ - async save(data) { - if (typeof data === 'object') { - data = { ...data } - for (const [k, v] of Object.entries(data)) { - if (v === undefined) { - delete data[k] - } - } - } - this.#config.set(data) - } - - /** @returns {Promise} */ - async load() { - const data = this.#config.store ?? {} - if (Object.keys(data).length === 0) return - return data - } -} +export * from '@web3-storage/w3up-client/driver/conf' diff --git a/packages/access-client/src/drivers/indexeddb.js b/packages/access-client/src/drivers/indexeddb.js index 2a77a0d8d..695ca7b5c 100644 --- a/packages/access-client/src/drivers/indexeddb.js +++ b/packages/access-client/src/drivers/indexeddb.js @@ -1,193 +1 @@ -import defer from 'p-defer' - -/** - * @template T - * @typedef {import('./types.js').Driver} Driver - */ - -const STORE_NAME = 'AccessStore' -const DATA_ID = 1 - -/** - * Driver implementation for the browser. - * - * Usage: - * - * ```js - * import { IndexedDBDriver } from '@web3-storage/access/drivers/indexeddb' - * ``` - * - * @template T - * @implements {Driver} - */ -export class IndexedDBDriver { - /** @type {string} */ - #dbName - - /** @type {number|undefined} */ - #dbVersion - - /** @type {string} */ - #dbStoreName - - /** @type {IDBDatabase|undefined} */ - #db - - /** @type {boolean} */ - #autoOpen - - /** - * @param {string} dbName - * @param {object} [options] - * @param {number} [options.dbVersion] - * @param {string} [options.dbStoreName] - * @param {boolean} [options.autoOpen] - */ - constructor(dbName, options = {}) { - this.#dbName = dbName - this.#dbVersion = options.dbVersion - this.#dbStoreName = options.dbStoreName ?? STORE_NAME - this.#autoOpen = options.autoOpen ?? true - } - - /** @returns {Promise} */ - async #getOpenDB() { - if (!this.#db) { - if (!this.#autoOpen) throw new Error('Store is not open') - await this.open() - } - // @ts-expect-error open sets this.#db - return this.#db - } - - async open() { - const db = this.#db - if (db) return - - /** @type {import('p-defer').DeferredPromise} */ - const { resolve, reject, promise } = defer() - const openReq = indexedDB.open(this.#dbName, this.#dbVersion) - - openReq.addEventListener('upgradeneeded', () => { - const db = openReq.result - db.createObjectStore(this.#dbStoreName, { keyPath: 'id' }) - }) - - openReq.addEventListener('success', () => { - this.#db = openReq.result - resolve() - }) - - openReq.addEventListener('error', () => reject(openReq.error)) - - return promise - } - - async close() { - const db = this.#db - if (!db) throw new Error('Store is not open') - - db.close() - this.#db = undefined - } - - /** @param {T} data */ - async save(data) { - const db = await this.#getOpenDB() - - const putData = withObjectStore( - db, - 'readwrite', - this.#dbStoreName, - async (store) => { - /** @type {import('p-defer').DeferredPromise} */ - const { resolve, reject, promise } = defer() - const putReq = store.put({ id: DATA_ID, ...data }) - putReq.addEventListener('success', () => resolve()) - putReq.addEventListener('error', () => - reject(new Error('failed to query DB', { cause: putReq.error })) - ) - - return promise - } - ) - - return await putData() - } - - async load() { - const db = await this.#getOpenDB() - - const getData = withObjectStore( - db, - 'readonly', - this.#dbStoreName, - async (store) => { - /** @type {import('p-defer').DeferredPromise} */ - const { resolve, reject, promise } = defer() - - const getReq = store.get(DATA_ID) - getReq.addEventListener('success', () => resolve(getReq.result)) - getReq.addEventListener('error', () => - reject(new Error('failed to query DB', { cause: getReq.error })) - ) - - return promise - } - ) - - return await getData() - } - - async reset() { - const db = await this.#getOpenDB() - - withObjectStore(db, 'readwrite', this.#dbStoreName, (s) => { - /** @type {import('p-defer').DeferredPromise} */ - const { resolve, reject, promise } = defer() - const req = s.clear() - req.addEventListener('success', () => { - resolve() - }) - - req.addEventListener('error', () => - reject(new Error('failed to query DB', { cause: req.error })) - ) - - return promise - }) - } -} - -/** - * @template T - * @param {IDBDatabase} db - * @param {IDBTransactionMode} txnMode - * @param {string} storeName - * @param {(s: IDBObjectStore) => Promise} fn - * @returns - */ -function withObjectStore(db, txnMode, storeName, fn) { - return async () => { - const tx = db.transaction(storeName, txnMode) - /** @type {import('p-defer').DeferredPromise} */ - const { resolve, reject, promise } = defer() - /** @type {T} */ - let result - tx.addEventListener('complete', () => resolve(result)) - tx.addEventListener('abort', () => - reject(tx.error || new Error('transaction aborted')) - ) - tx.addEventListener('error', () => - reject(new Error('transaction error', { cause: tx.error })) - ) - try { - result = await fn(tx.objectStore(storeName)) - tx.commit() - } catch (error) { - reject(error) - tx.abort() - } - return promise - } -} +export * from '@web3-storage/w3up-client/driver/indexed-db' diff --git a/packages/access-client/src/drivers/memory.js b/packages/access-client/src/drivers/memory.js index 9b89f185c..563cf4fe9 100644 --- a/packages/access-client/src/drivers/memory.js +++ b/packages/access-client/src/drivers/memory.js @@ -1,47 +1 @@ -/** - * @template T - * @typedef {import('./types.js').Driver} Driver - */ - -/** - * Driver implementation that stores data in memory." - * - * Usage: - * - * ```js - * import { MemoryDriver } from '@web3-storage/access/drivers/memory' - * ``` - * - * @template {Record} T - * @implements {Driver} - */ -export class MemoryDriver { - /** - * @type {T|undefined} - */ - #data - - constructor() { - this.#data = undefined - } - - async open() {} - - async close() {} - - async reset() { - this.#data = undefined - } - - /** @param {T} data */ - async save(data) { - this.#data = { ...data } - } - - /** @returns {Promise} */ - async load() { - if (this.#data === undefined) return - if (Object.keys(this.#data).length === 0) return - return this.#data - } -} +export * from '@web3-storage/w3up-client/driver/memory' diff --git a/packages/access-client/src/encoding.js b/packages/access-client/src/encoding.js index 479412588..acaa7144f 100644 --- a/packages/access-client/src/encoding.js +++ b/packages/access-client/src/encoding.js @@ -1,156 +1 @@ -/** - * Encoding utilities - * - * It is recommended that you import directly with: - * ```js - * import * as Encoding from '@web3-storage/access/encoding' - * - * // or - * - * import { encodeDelegations } from '@web3-storage/access/encoding' - * ``` - * - * @module - */ -import { CarBufferReader } from '@ipld/car/buffer-reader' -import * as CarBufferWriter from '@ipld/car/buffer-writer' -import { Delegation } from '@ucanto/core/delegation' -import * as u8 from 'uint8arrays' -// eslint-disable-next-line no-unused-vars -import * as Types from '@ucanto/interface' - -/** - * Encode delegations as bytes - * - * @param {Types.Delegation[]} delegations - */ -export function delegationsToBytes(delegations) { - if (!Array.isArray(delegations) || delegations.length === 0) { - throw new Error('Delegations required to be an non empty array.') - } - - const roots = delegations.map( - (d) => /** @type {CarBufferWriter.CID} */ (d.root.cid) - ) - const cids = new Set() - /** @type {CarBufferWriter.Block[]} */ - const blocks = [] - let byteLength = 0 - - for (const delegation of delegations) { - for (const block of delegation.export()) { - const cid = block.cid.toV1().toString() - if (!cids.has(cid)) { - byteLength += CarBufferWriter.blockLength( - /** @type {CarBufferWriter.Block} */ (block) - ) - blocks.push(/** @type {CarBufferWriter.Block} */ (block)) - cids.add(cid) - } - } - } - const headerLength = CarBufferWriter.estimateHeaderLength(roots.length) - const writer = CarBufferWriter.createWriter( - new ArrayBuffer(headerLength + byteLength), - { roots } - ) - for (const block of blocks) { - writer.write(block) - } - - return writer.close() -} - -/** - * Decode bytes into Delegations - * - * @template {Types.Capabilities} [T=Types.Capabilities] - * @param {import('./types.js').BytesDelegation} bytes - */ -export function bytesToDelegations(bytes) { - if (!(bytes instanceof Uint8Array) || bytes.length === 0) { - throw new TypeError('Input should be a non-empty Uint8Array.') - } - const reader = CarBufferReader.fromBytes(bytes) - const roots = reader.getRoots() - - /** @type {Types.Delegation[]} */ - const delegations = [] - - for (const root of roots) { - const rootBlock = reader.get(root) - - if (rootBlock) { - const blocks = new Map() - for (const block of reader.blocks()) { - if (block.cid.toString() !== root.toString()) - blocks.set(block.cid.toString(), block) - } - - // @ts-ignore - delegations.push(new Delegation(rootBlock, blocks)) - } else { - throw new Error('Failed to find root from raw delegation.') - } - } - - return delegations -} - -/** - * @param {Types.Delegation[]} delegations - * @param {import('uint8arrays/to-string').SupportedEncodings} encoding - */ -export function delegationsToString(delegations, encoding = 'base64url') { - const bytes = delegationsToBytes(delegations) - - return u8.toString(bytes, encoding) -} - -/** - * Encode one {@link Types.Delegation Delegation} into a string - * - * @param {Types.Delegation} delegation - * @param {import('uint8arrays/to-string').SupportedEncodings} [encoding] - */ -export function delegationToString(delegation, encoding) { - return delegationsToString([delegation], encoding) -} - -/** - * Decode string into {@link Types.Delegation Delegation} - * - * @template {Types.Capabilities} [T=Types.Capabilities] - * @param {import('./types.js').EncodedDelegation} raw - * @param {import('uint8arrays/to-string').SupportedEncodings} [encoding] - */ -export function stringToDelegations(raw, encoding = 'base64url') { - const bytes = u8.fromString(raw, encoding) - - return bytesToDelegations(bytes) -} - -/** - * Decode string into a {@link Types.Delegation Delegation} - * - * @template {Types.Capabilities} [T=Types.Capabilities] - * @param {import('./types.js').EncodedDelegation} raw - * @param {import('uint8arrays/to-string').SupportedEncodings} [encoding] - */ -export function stringToDelegation(raw, encoding) { - const delegations = stringToDelegations(raw, encoding) - - return /** @type {Types.Delegation} */ (delegations[0]) -} - -/** - * @param {number} [expiration] - */ -export function expirationToDate(expiration) { - const expires = - expiration === Infinity || !expiration - ? undefined - : new Date(expiration * 1000) - - return expires -} +export * from '@web3-storage/w3up-client/agent/encoding' diff --git a/packages/access-client/src/index.js b/packages/access-client/src/index.js index 70fcc18cf..8ccd0b82b 100644 --- a/packages/access-client/src/index.js +++ b/packages/access-client/src/index.js @@ -1,8 +1,7 @@ -/* eslint-disable jsdoc/check-tag-names */ -export * from './agent.js' +export * from '@web3-storage/w3up-client/agent' -// Workaround for typedoc until 0.24 support export maps -export * as Encoding from './encoding.js' -export { StoreConf } from './stores/store-conf.js' -export { StoreIndexedDB } from './stores/store-indexeddb.js' -export { StoreMemory } from './stores/store-memory.js' +// Workaround for typedoc until 0.24 supports export maps +export * as Encoding from '@web3-storage/w3up-client/agent/encoding' +export { StoreConf } from '@web3-storage/w3up-client/store/conf' +export { StoreIndexedDB } from '@web3-storage/w3up-client/store/indexed-db' +export { StoreMemory } from '@web3-storage/w3up-client/store/memory' diff --git a/packages/access-client/src/provider.js b/packages/access-client/src/provider.js index 42717175f..973954a19 100644 --- a/packages/access-client/src/provider.js +++ b/packages/access-client/src/provider.js @@ -1,45 +1 @@ -import * as API from './types.js' -import * as Provider from '@web3-storage/capabilities/provider' - -export const { Provider: ProviderDID, AccountDID } = Provider - -/** - * Provisions specified `space` with the specified `account`. It is expected - * that delegation from the account authorizing agent is either stored in the - * agent proofs or provided explicitly. - * - * @template {Record} [S=API.Service] - * @param {API.Agent} agent - * @param {object} input - * @param {API.AccountDID} input.account - Account provisioning the space. - * @param {API.SpaceDID} input.consumer - Space been provisioned. - * @param {API.ProviderDID} [input.provider] - Provider been provisioned. - * @param {API.Delegation[]} [input.proofs] - Delegation from the account - * authorizing agent to call `provider/add` capability. - */ -export const add = async ( - agent, - { - account, - consumer, - provider = /** @type {API.ProviderDID} */ (agent.connection.id.did()), - proofs, - } -) => { - if (!ProviderDID.is(provider)) { - throw new Error( - `Unable to determine provider from agent.connection.id did ${provider}. expected a did:web:` - ) - } - - const { out } = await agent.invokeAndExecute(Provider.add, { - with: account, - nb: { - provider, - consumer, - }, - proofs, - }) - - return out -} +export * from '@web3-storage/w3up-client/capability/provider' diff --git a/packages/access-client/src/space.js b/packages/access-client/src/space.js index a338d15c9..d10e6db81 100644 --- a/packages/access-client/src/space.js +++ b/packages/access-client/src/space.js @@ -1,264 +1 @@ -import * as ED25519 from '@ucanto/principal/ed25519' -import { delegate, Schema, UCAN } from '@ucanto/core' -import * as BIP39 from '@scure/bip39' -import { wordlist } from '@scure/bip39/wordlists/english' -import * as API from './types.js' -import * as Access from './access.js' - -/** - * Data model for the (owned) space. - * - * @typedef {object} Model - * @property {ED25519.EdSigner} signer - * @property {string} name - */ - -/** - * Generates a new space. - * - * @param {object} options - * @param {string} options.name - */ -export const generate = async ({ name }) => { - const { signer } = await ED25519.generate() - - return new OwnedSpace({ signer, name }) -} - -/** - * Recovers space from the saved mnemonic. - * - * @param {string} mnemonic - * @param {object} options - * @param {string} options.name - Name to give to the recovered space. - */ -export const fromMnemonic = async (mnemonic, { name }) => { - const secret = BIP39.mnemonicToEntropy(mnemonic, wordlist) - const signer = await ED25519.derive(secret) - return new OwnedSpace({ signer, name }) -} - -/** - * Turns (owned) space into a BIP39 mnemonic that later can be used to recover - * the space using `fromMnemonic` function. - * - * @param {object} space - * @param {ED25519.EdSigner} space.signer - */ -export const toMnemonic = ({ signer }) => { - /** @type {Uint8Array} */ - // @ts-expect-error - Field is defined but not in the interface - const secret = signer.secret - - return BIP39.entropyToMnemonic(secret, wordlist) -} - -/** - * Creates a (UCAN) delegation that gives full access to the space to the - * specified `account`. At the moment we only allow `did:mailto` principal - * to be used as an `account`. - * - * @param {Model} space - * @param {API.AccountDID} account - */ -export const createRecovery = (space, account) => - createAuthorization(space, { - agent: space.signer.withDID(account), - access: Access.accountAccess, - expiration: Infinity, - }) - -// Default authorization session is valid for 1 year -export const SESSION_LIFETIME = 60 * 60 * 24 * 365 - -/** - * Creates (UCAN) delegation that gives specified `agent` an access to - * specified ability (passed as `access.can` field) on this space. - * Optionally, you can specify `access.expiration` field to set the - * expiration time for the authorization. By default the authorization - * is valid for 1 year and gives access to all capabilities on the space - * that are needed to use the space. - * - * @param {Model} space - * @param {object} options - * @param {API.Principal} options.agent - * @param {API.Access} [options.access] - * @param {API.UTCUnixTimestamp} [options.expiration] - */ -export const createAuthorization = async ( - { signer, name }, - { - agent, - access = Access.spaceAccess, - expiration = UCAN.now() + SESSION_LIFETIME, - } -) => { - return await delegate({ - issuer: signer, - audience: agent, - capabilities: toCapabilities({ - [signer.did()]: access, - }), - ...(expiration ? { expiration } : {}), - facts: [{ space: { name } }], - }) -} - -/** - * @param {Record} allow - * @returns {API.Capabilities} - */ -const toCapabilities = (allow) => { - const capabilities = [] - for (const [subject, access] of Object.entries(allow)) { - const entries = /** @type {[API.Ability, API.Unit][]} */ ( - Object.entries(access) - ) - - for (const [can, details] of entries) { - if (details) { - capabilities.push({ can, with: subject }) - } - } - } - - return /** @type {API.Capabilities} */ (capabilities) -} - -/** - * Represents an owned space, meaning a space for which we have a private key - * and consequently have full authority over. - */ -class OwnedSpace { - /** - * @param {Model} model - */ - constructor(model) { - this.model = model - } - - get signer() { - return this.model.signer - } - - get name() { - return this.model.name - } - - did() { - return this.signer.did() - } - - /** - * Creates a renamed version of this space. - * - * @param {string} name - */ - withName(name) { - return new OwnedSpace({ signer: this.signer, name }) - } - - /** - * Creates a (UCAN) delegation that gives full access to the space to the - * specified `account`. At the moment we only allow `did:mailto` principal - * to be used as an `account`. - * - * @param {API.AccountDID} account - */ - async createRecovery(account) { - return createRecovery(this, account) - } - - /** - * Creates (UCAN) delegation that gives specified `agent` an access to - * specified ability (passed as `access.can` field) on the this space. - * Optionally, you can specify `access.expiration` field to set the - * - * @param {API.Principal} agent - * @param {object} [input] - * @param {API.Access} [input.access] - * @param {API.UCAN.UTCUnixTimestamp} [input.expiration] - */ - createAuthorization(agent, input) { - return createAuthorization(this, { ...input, agent }) - } - - /** - * Derives BIP39 mnemonic that can be used to recover the space. - * - * @returns {string} - */ - toMnemonic() { - return toMnemonic(this) - } -} - -const SpaceDID = Schema.did({ method: 'key' }) - -/** - * Creates a (shared) space from given delegation. - * - * @param {API.Delegation} delegation - */ -export const fromDelegation = (delegation) => { - const result = SpaceDID.read(delegation.capabilities[0].with) - if (result.error) { - throw Object.assign( - new Error( - `Invalid delegation, expected capabilities[0].with to be DID, ${result.error}` - ), - { - cause: result.error, - } - ) - } - - /** @type {{name?:string}} */ - const meta = delegation.facts[0]?.space ?? {} - - return new SharedSpace({ id: result.ok, delegation, meta }) -} - -/** - * Represents a shared space, meaning a space for which we have a delegation - * and consequently have limited authority over. - */ -class SharedSpace { - /** - * @typedef {object} SharedSpaceModel - * @property {API.SpaceDID} id - * @property {API.Delegation} delegation - * @property {{name?:string}} meta - * - * @param {SharedSpaceModel} model - */ - constructor(model) { - this.model = model - } - - get delegation() { - return this.model.delegation - } - - get meta() { - return this.model.meta - } - - get name() { - return this.meta.name ?? '' - } - - did() { - return this.model.id - } - - /** - * @param {string} name - */ - withName(name) { - return new SharedSpace({ - ...this.model, - meta: { ...this.meta, name }, - }) - } -} +export * from '@web3-storage/w3up-client/capability/space' diff --git a/packages/access-client/src/stores/conf.js b/packages/access-client/src/stores/conf.js new file mode 100644 index 000000000..65f3273f5 --- /dev/null +++ b/packages/access-client/src/stores/conf.js @@ -0,0 +1 @@ +export * from '@web3-storage/w3up-client/store/conf' diff --git a/packages/access-client/src/stores/indexeddb.js b/packages/access-client/src/stores/indexeddb.js new file mode 100644 index 000000000..3d30cae01 --- /dev/null +++ b/packages/access-client/src/stores/indexeddb.js @@ -0,0 +1 @@ +export * from '@web3-storage/w3up-client/store/indexed-db' diff --git a/packages/access-client/src/stores/memory.js b/packages/access-client/src/stores/memory.js new file mode 100644 index 000000000..ceb52bd99 --- /dev/null +++ b/packages/access-client/src/stores/memory.js @@ -0,0 +1 @@ +export * from '@web3-storage/w3up-client/store/memory' diff --git a/packages/access-client/src/stores/store-indexeddb.js b/packages/access-client/src/stores/store-indexeddb.js index 68071efc1..3d30cae01 100644 --- a/packages/access-client/src/stores/store-indexeddb.js +++ b/packages/access-client/src/stores/store-indexeddb.js @@ -1,14 +1 @@ -import { IndexedDBDriver } from '../drivers/indexeddb.js' - -/** - * Store implementation for the browser. - * - * Usage: - * - * ```js - * import { StoreIndexedDB } from '@web3-storage/access/stores/store-indexeddb' - * ``` - * - * @extends {IndexedDBDriver} - */ -export class StoreIndexedDB extends IndexedDBDriver {} +export * from '@web3-storage/w3up-client/store/indexed-db' diff --git a/packages/access-client/src/types.js b/packages/access-client/src/types.js index 336ce12bb..1d98ce004 100644 --- a/packages/access-client/src/types.js +++ b/packages/access-client/src/types.js @@ -1 +1 @@ -export {} +export * from '@web3-storage/w3up-client/agent' diff --git a/packages/access-client/test/helpers/utils.js b/packages/access-client/test/helpers/utils.js deleted file mode 100644 index f377a0d49..000000000 --- a/packages/access-client/test/helpers/utils.js +++ /dev/null @@ -1,66 +0,0 @@ -// eslint-disable-next-line no-unused-vars -import * as Ucanto from '@ucanto/interface' -import { parseLink } from '@ucanto/core' -import * as Server from '@ucanto/server' -import * as Space from '@web3-storage/capabilities/space' -import * as CAR from '@ucanto/transport/car' -import * as CBOR from '@ucanto/core/cbor' -import { service } from './fixtures.js' - -/** - * @param {string} source - */ -export function parseCarLink(source) { - return /** @type {Ucanto.Link} */ (parseLink(source)) -} - -/** - * @param {any} data - */ -export async function createCborCid(data) { - const cbor = await CBOR.write(data) - return cbor.cid -} - -/** - * @param {string} source - */ -export async function createCarCid(source) { - const cbor = await CBOR.write({ hello: source }) - const shard = await CAR.codec.write({ roots: [cbor] }) - return shard.cid -} - -/** - * @param {object} handlers - a map of keys to capability handler maps - * @returns {Ucanto.ServerView} - */ -export function createServer(handlers = {}) { - const server = Server.create({ - id: service, - codec: CAR.inbound, - service: { - space: { - info: Server.provide(Space.info, async ({ capability }) => { - return { - ok: { - did: 'did:key:sss', - agent: 'did:key:agent', - email: 'mail@mail.com', - product: 'product:free', - updated_at: 'sss', - inserted_at: 'date', - }, - } - }), - }, - ...handlers, - }, - validateAuthorization, - }) - - // @ts-ignore - return server -} - -export const validateAuthorization = () => ({ ok: {} }) diff --git a/packages/access-client/tsconfig.json b/packages/access-client/tsconfig.json index 186028130..0e14aa5aa 100644 --- a/packages/access-client/tsconfig.json +++ b/packages/access-client/tsconfig.json @@ -6,5 +6,5 @@ }, "include": ["src", "scripts", "test", "package.json"], "exclude": ["**/node_modules/**"], - "references": [{ "path": "../capabilities" }, { "path": "../did-mailto" }] + "references": [{ "path": "../w3up-client" }] } diff --git a/packages/filecoin-api/test/context/queue-implementations.js b/packages/filecoin-api/test/context/queue-implementations.js index 2785a2210..a75936644 100644 --- a/packages/filecoin-api/test/context/queue-implementations.js +++ b/packages/filecoin-api/test/context/queue-implementations.js @@ -2,6 +2,7 @@ import { Queue } from './queue.js' /** * @param {Map} queuedMessages + * @param QueueImplementation */ export const getQueueImplementations = ( queuedMessages, diff --git a/packages/filecoin-api/test/context/store-implementations.js b/packages/filecoin-api/test/context/store-implementations.js index 5c47f336e..a576ba198 100644 --- a/packages/filecoin-api/test/context/store-implementations.js +++ b/packages/filecoin-api/test/context/store-implementations.js @@ -1,6 +1,7 @@ import { UpdatableStore } from './store.js' /** + * @param StoreImplementation * @typedef {import('@ucanto/interface').Link} Link * @typedef {import('../../src/storefront/api.js').PieceRecord} PieceRecord * @typedef {import('../../src/storefront/api.js').PieceRecordKey} PieceRecordKey diff --git a/packages/filecoin-client/package.json b/packages/filecoin-client/package.json index 711ee9d8e..7c8796516 100644 --- a/packages/filecoin-client/package.json +++ b/packages/filecoin-client/package.json @@ -24,11 +24,31 @@ "rc": "npm version prerelease --preid rc" }, "exports": { - ".": "./dist/src/index.js", - "./aggregator": "./dist/src/aggregator.js", - "./dealer": "./dist/src/dealer.js", - "./storefront": "./dist/src/storefront.js", - "./types": "./dist/src/types.js" + ".": { + "types": "./dist/src/index.d.ts", + "import": "./src/index.js", + "default": "./dist/src/index.js" + }, + "./aggregator": { + "types": "./dist/src/aggregator.d.ts", + "import": "./src/aggregator.js", + "default": "./dist/src/aggregator.js" + }, + "./dealer": { + "types": "./dist/src/dealer.d.ts", + "import": "./src/dealer.js", + "default": "./dist/src/dealer.js" + }, + "./storefront": { + "types": "./dist/src/storefront.d.ts", + "import": "./src/storefront.js", + "default": "./dist/src/storefront.js" + }, + "./types": { + "types": "./dist/src/types.d.ts", + "import": "./src/types.js", + "default": "./dist/src/types.js" + } }, "typesVersions": { "*": { diff --git a/packages/upload-api/README.md b/packages/upload-api/README.md index d0484a415..66582a476 100644 --- a/packages/upload-api/README.md +++ b/packages/upload-api/README.md @@ -1,29 +1,3 @@ -


web3.storage

-

The upload API for https://web3.storage

- -## About - -The `@web3-storage/upload-api` package provides an implementation of the w3up -UCAN invocation service. It provides a set of storage interfaces that can -be implemented to run w3up on top of arbitrary data stores. - -## Install - -Install the package using npm: - -```bash -npm install @web3-storage/upload-api -``` - -## Usage - -Coming soon! - -## Contributing - -Feel free to join in. All welcome. Please [open an issue](https://github.com/web3-storage/w3up/issues)! - -## License - -Dual-licensed under [MIT + Apache 2.0](https://github.com/web3-storage/w3up/blob/main/license.md) +# ⚠️ Deprecated +Use `@web3-storage/w3up-client` instead. diff --git a/packages/upload-api/package.json b/packages/upload-api/package.json index ec262a45e..04e950892 100644 --- a/packages/upload-api/package.json +++ b/packages/upload-api/package.json @@ -114,7 +114,7 @@ "@ucanto/server": "^9.0.1", "@ucanto/transport": "^9.0.0", "@ucanto/validator": "^9.0.0", - "@web3-storage/access": "workspace:^", + "@web3-storage/w3up-client": "workspace:^", "@web3-storage/capabilities": "workspace:^", "@web3-storage/did-mailto": "workspace:^", "@web3-storage/filecoin-api": "workspace:^", diff --git a/packages/upload-api/src/access/authorize.js b/packages/upload-api/src/access/authorize.js index a0417d413..8bd1f2484 100644 --- a/packages/upload-api/src/access/authorize.js +++ b/packages/upload-api/src/access/authorize.js @@ -2,7 +2,7 @@ import * as Server from '@ucanto/server' import * as API from '../types.js' import * as Access from '@web3-storage/capabilities/access' import * as DidMailto from '@web3-storage/did-mailto' -import { delegationToString } from '@web3-storage/access/encoding' +import { delegationToString } from '@web3-storage/w3up-client/agent/encoding' import { mailtoDidToDomain, mailtoDidToEmail } from '../utils/did-mailto.js' import { ensureRateLimitAbove } from '../utils/rate-limits.js' diff --git a/packages/upload-api/src/space.js b/packages/upload-api/src/space.js index 3faf937f6..e24cac501 100644 --- a/packages/upload-api/src/space.js +++ b/packages/upload-api/src/space.js @@ -34,7 +34,7 @@ export const info = async ({ capability }, ctx) => { } } - /** @type {import('@web3-storage/access/types').SpaceUnknown} */ + /** @type {import('@web3-storage/w3up-client').SpaceUnknown} */ const spaceUnknownFailure = { name: 'SpaceUnknown', message: `Space not found.`, diff --git a/packages/upload-api/src/utils/delegations-response.js b/packages/upload-api/src/utils/delegations-response.js index 6a0a46109..e54dd9a8e 100644 --- a/packages/upload-api/src/utils/delegations-response.js +++ b/packages/upload-api/src/utils/delegations-response.js @@ -7,7 +7,7 @@ import * as Ucanto from '@ucanto/interface' import { bytesToDelegations, delegationsToBytes, -} from '@web3-storage/access/encoding' +} from '@web3-storage/w3up-client/agent/encoding' /** * @template D @@ -37,7 +37,7 @@ export function encode(delegations) { export function* decode(encoded) { for (const carBytes of Object.values(encoded)) { const delegations = bytesToDelegations( - /** @type {import('@web3-storage/access/types').BytesDelegation} */ ( + /** @type {import('@web3-storage/w3up-client').BytesDelegation} */ ( carBytes ) ) diff --git a/packages/upload-api/src/validate.js b/packages/upload-api/src/validate.js index 5d21755ae..9331dfbb7 100644 --- a/packages/upload-api/src/validate.js +++ b/packages/upload-api/src/validate.js @@ -1,7 +1,7 @@ import { delegationsToString, stringToDelegation, -} from '@web3-storage/access/encoding' +} from '@web3-storage/w3up-client/agent/encoding' import * as DidMailto from '@web3-storage/did-mailto' import { Verifier } from '@ucanto/principal' import * as delegationsResponse from './utils/delegations-response.js' diff --git a/packages/upload-api/test/access-client-agent.js b/packages/upload-api/test/access-client-agent.js index 3f805cca9..f78f357e8 100644 --- a/packages/upload-api/test/access-client-agent.js +++ b/packages/upload-api/test/access-client-agent.js @@ -3,16 +3,15 @@ import { Absentee } from '@ucanto/principal' import * as delegationsResponse from '../src/utils/delegations-response.js' import * as DidMailto from '@web3-storage/did-mailto' import { Access, Space } from '@web3-storage/capabilities' -import { AgentData } from '@web3-storage/access' +import { AgentData } from '@web3-storage/w3up-client/agent' import { alice } from './helpers/utils.js' -import { stringToDelegations } from '@web3-storage/access/encoding' +import { stringToDelegations } from '@web3-storage/w3up-client/agent/encoding' import { confirmConfirmationUrl, extractConfirmInvocation, } from './helpers/utils.js' import { Agent, - Access as AgentAccess, claimAccess, addProvider, authorizeAndWait, @@ -21,8 +20,9 @@ import { delegationsIncludeSessionProof, addSpacesFromDelegations, requestAccess, -} from '@web3-storage/access/agent' -import * as Provider from '@web3-storage/access/provider' +} from '@web3-storage/w3up-client/agent' +import * as AgentAccess from '@web3-storage/w3up-client/capability/access' +import * as Provider from '@web3-storage/w3up-client/capability/provider' /** * @type {API.Tests} @@ -313,7 +313,7 @@ export const test = { assert.ok(spaceInfoResult.out.ok) const result = - /** @type {import('@web3-storage/access/types').SpaceInfoResult} */ ( + /** @type {import('@web3-storage/w3up-client').SpaceInfoResult} */ ( spaceInfoResult.out.ok ) assert.deepEqual(result.did, spaceCreation.did()) diff --git a/packages/upload-api/test/handlers/access/authorize.js b/packages/upload-api/test/handlers/access/authorize.js index 0293c8df3..46689e1bb 100644 --- a/packages/upload-api/test/handlers/access/authorize.js +++ b/packages/upload-api/test/handlers/access/authorize.js @@ -7,7 +7,7 @@ import * as DidMailto from '@web3-storage/did-mailto' import { stringToDelegation, bytesToDelegations, -} from '@web3-storage/access/encoding' +} from '@web3-storage/w3up-client/agent/encoding' import { authorizeFromUrl } from '../../../src/validate.js' /** @@ -36,7 +36,7 @@ export const test = { const url = new URL(email.url) const encoded = - /** @type {import('@web3-storage/access/types').EncodedDelegation<[import('@web3-storage/capabilities/types').AccessConfirm]>} */ ( + /** @type {import('@web3-storage/w3up-client').EncodedDelegation<[import('@web3-storage/capabilities/types').AccessConfirm]>} */ ( url.searchParams.get('ucan') ) const delegation = stringToDelegation(encoded) diff --git a/packages/upload-api/test/helpers/utils.js b/packages/upload-api/test/helpers/utils.js index 3581f1565..6374e71c9 100644 --- a/packages/upload-api/test/helpers/utils.js +++ b/packages/upload-api/test/helpers/utils.js @@ -8,7 +8,7 @@ import * as Context from './context.js' import { Provider, UCAN, Space } from '@web3-storage/capabilities' import * as DidMailto from '@web3-storage/did-mailto' import * as API from '../types.js' -import { stringToDelegation } from '@web3-storage/access/encoding' +import { stringToDelegation } from '@web3-storage/w3up-client/agent/encoding' export { Context } @@ -202,7 +202,7 @@ export const queue = (buffer = []) => { /** * @param {Types.Signer} issuer * @param {Types.Signer} service - * @param {Types.ConnectionView} conn + * @param {Types.ConnectionView} conn * @param {`${string}@${string}`} email */ export async function createSpace(issuer, service, conn, email) { @@ -254,7 +254,7 @@ export async function extractConfirmInvocation(confirmationUrl) { } /** - * @param {API.ConnectionView} connection + * @param {API.ConnectionView} connection * @param {{ url: string|URL }} confirmation */ export async function confirmConfirmationUrl(connection, confirmation) { diff --git a/packages/upload-api/tsconfig.json b/packages/upload-api/tsconfig.json index ec01de0e1..89380dc41 100644 --- a/packages/upload-api/tsconfig.json +++ b/packages/upload-api/tsconfig.json @@ -6,5 +6,9 @@ }, "include": ["src", "test"], "exclude": ["**/node_modules/**", "dist"], - "references": [{ "path": "../capabilities" }, { "path": "../access-client"}, { "path": "../filecoin-api" }] + "references": [ + { "path": "../capabilities" }, + { "path": "../filecoin-api" }, + { "path": "../w3up-client" } + ] } diff --git a/packages/upload-client/package.json b/packages/upload-client/package.json index 95ced970f..181c3c2a9 100644 --- a/packages/upload-client/package.json +++ b/packages/upload-client/package.json @@ -18,14 +18,6 @@ "lint": "eslint '**/*.{js,ts}' && prettier --check '**/*.{js,ts,yml,json}' --ignore-path ../../.gitignore", "build": "tsc --build", "check": "tsc --build", - "test": "npm-run-all -p -r mock test:all", - "test:all": "run-s test:node test:browser", - "test:node": "hundreds -r html -r text mocha 'test/**/!(*.browser).test.js' -n experimental-vm-modules -n no-warnings", - "test:browser": "playwright-test 'test/**/!(*.node).test.js'", - "mock": "run-p mock:*", - "mock:bucket-200": "PORT=9200 STATUS=200 node test/helpers/bucket-server.js", - "mock:bucket-401": "PORT=9400 STATUS=400 node test/helpers/bucket-server.js", - "mock:bucket-500": "PORT=9500 STATUS=500 node test/helpers/bucket-server.js", "rc": "npm version prerelease --preid rc" }, "exports": { @@ -66,36 +58,10 @@ "dist/src/**/*.d.ts.map" ], "dependencies": { - "@ipld/car": "^5.2.2", - "@ipld/dag-cbor": "^9.0.6", - "@ipld/dag-ucan": "^3.4.0", - "@ipld/unixfs": "^2.1.1", - "@ucanto/client": "^9.0.0", - "@ucanto/interface": "^9.0.0", - "@ucanto/transport": "^9.0.0", - "@web3-storage/capabilities": "workspace:^", - "fr32-sha2-256-trunc254-padded-binary-tree-multihash": "^3.1.0", - "ipfs-utils": "^9.0.14", - "multiformats": "^12.1.2", - "p-retry": "^5.1.2", - "parallel-transform-web": "^1.0.0", - "varint": "^6.0.0" + "@web3-storage/w3up-client": "workspace:^" }, "devDependencies": { - "@types/assert": "^1.5.6", - "@types/mocha": "^10.0.1", - "@types/varint": "^6.0.1", "@web3-storage/eslint-config-w3up": "workspace:^", - "@ucanto/principal": "^9.0.0", - "@ucanto/server": "^9.0.1", - "assert": "^2.0.0", - "blockstore-core": "^3.0.0", - "c8": "^7.13.0", - "hundreds": "^0.0.9", - "ipfs-unixfs-exporter": "^10.0.0", - "mocha": "^10.2.0", - "npm-run-all": "^4.1.5", - "playwright-test": "^12.3.4", "typescript": "5.2.2" }, "eslintConfig": { diff --git a/packages/upload-client/src/car.js b/packages/upload-client/src/car.js index 773170a40..4fb4fcf36 100644 --- a/packages/upload-client/src/car.js +++ b/packages/upload-client/src/car.js @@ -1,115 +1 @@ -import { CarBlockIterator, CarWriter } from '@ipld/car' -import * as dagCBOR from '@ipld/dag-cbor' -import varint from 'varint' - -/** - * @typedef {import('@ipld/unixfs').Block} Block - */ - -/** Byte length of a CBOR encoded CAR header with zero roots. */ -const NO_ROOTS_HEADER_LENGTH = 17 - -/** @param {import('./types.js').AnyLink} [root] */ -export function headerEncodingLength(root) { - if (!root) return NO_ROOTS_HEADER_LENGTH - const headerLength = dagCBOR.encode({ version: 1, roots: [root] }).length - const varintLength = varint.encodingLength(headerLength) - return varintLength + headerLength -} - -/** @param {Block} block */ -export function blockEncodingLength(block) { - const payloadLength = block.cid.bytes.length + block.bytes.length - const varintLength = varint.encodingLength(payloadLength) - return varintLength + payloadLength -} - -/** - * @param {Iterable | AsyncIterable} blocks - * @param {import('./types.js').AnyLink} [root] - * @returns {Promise} - */ -export async function encode(blocks, root) { - // @ts-expect-error - const { writer, out } = CarWriter.create(root) - /** @type {Error?} */ - let error - void (async () => { - try { - for await (const block of blocks) { - await writer.put(block) - } - } catch (/** @type {any} */ err) { - error = err - } finally { - await writer.close() - } - })() - const chunks = [] - for await (const chunk of out) chunks.push(chunk) - // @ts-expect-error - if (error != null) throw error - const roots = root != null ? [root] : [] - return Object.assign(new Blob(chunks), { version: 1, roots }) -} - -/** @extends {ReadableStream} */ -export class BlockStream extends ReadableStream { - /** @param {import('./types.js').BlobLike} car */ - constructor(car) { - /** @type {Promise?} */ - let blocksPromise = null - const getBlocksIterable = () => { - if (blocksPromise) return blocksPromise - blocksPromise = CarBlockIterator.fromIterable(toIterable(car.stream())) - return blocksPromise - } - - /** @type {AsyncIterator?} */ - let iterator = null - super({ - async start() { - const blocks = await getBlocksIterable() - iterator = /** @type {AsyncIterator} */ ( - blocks[Symbol.asyncIterator]() - ) - }, - async pull(controller) { - /* c8 ignore next */ - if (!iterator) throw new Error('missing blocks iterator') - const { value, done } = await iterator.next() - if (done) return controller.close() - controller.enqueue(value) - }, - }) - - /** @returns {Promise} */ - this.getRoots = async () => { - const blocks = await getBlocksIterable() - return await blocks.getRoots() - } - } -} - -/* c8 ignore next 20 */ -/** - * @template T - * @param {{ getReader: () => ReadableStreamDefaultReader } | AsyncIterable} stream - * @returns {AsyncIterable} - */ -function toIterable(stream) { - return Symbol.asyncIterator in stream - ? stream - : (async function* () { - const reader = stream.getReader() - try { - while (true) { - const { done, value } = await reader.read() - if (done) return - yield value - } - } finally { - reader.releaseLock() - } - })() -} +export * from '@web3-storage/w3up-client/capability/upload/sharding' diff --git a/packages/upload-client/src/index.js b/packages/upload-client/src/index.js index 3336ee0fd..7f6e94e11 100644 --- a/packages/upload-client/src/index.js +++ b/packages/upload-client/src/index.js @@ -1,155 +1 @@ -import { Parallel } from 'parallel-transform-web' -import * as PieceHasher from 'fr32-sha2-256-trunc254-padded-binary-tree-multihash/async' -import * as Link from 'multiformats/link' -import * as raw from 'multiformats/codecs/raw' -import * as Store from './store.js' -import * as Upload from './upload.js' -import * as UnixFS from './unixfs.js' -import * as CAR from './car.js' -import { ShardingStream } from './sharding.js' - -export { Store, Upload, UnixFS, CAR } -export * from './sharding.js' - -const CONCURRENT_REQUESTS = 3 - -/** - * Uploads a file to the service and returns the root data CID for the - * generated DAG. - * - * Required delegated capability proofs: `store/add`, `upload/add` - * - * @param {import('./types.js').InvocationConfig} conf Configuration - * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. - * - * The `issuer` is the signing authority that is issuing the UCAN - * invocation(s). It is typically the user _agent_. - * - * The `with` is the resource the invocation applies to. It is typically the - * DID of a space. - * - * The `proofs` are a set of capability delegations that prove the issuer - * has the capability to perform the action. - * - * The issuer needs the `store/add` and `upload/add` delegated capability. - * @param {import('./types.js').BlobLike} file File data. - * @param {import('./types.js').UploadOptions} [options] - */ -export async function uploadFile(conf, file, options = {}) { - return await uploadBlockStream( - conf, - UnixFS.createFileEncoderStream(file), - options - ) -} - -/** - * Uploads a directory of files to the service and returns the root data CID - * for the generated DAG. All files are added to a container directory, with - * paths in file names preserved. - * - * Required delegated capability proofs: `store/add`, `upload/add` - * - * @param {import('./types.js').InvocationConfig} conf Configuration - * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. - * - * The `issuer` is the signing authority that is issuing the UCAN - * invocation(s). It is typically the user _agent_. - * - * The `with` is the resource the invocation applies to. It is typically the - * DID of a space. - * - * The `proofs` are a set of capability delegations that prove the issuer - * has the capability to perform the action. - * - * The issuer needs the `store/add` and `upload/add` delegated capability. - * @param {import('./types.js').FileLike[]} files File data. - * @param {import('./types.js').UploadDirectoryOptions} [options] - */ -export async function uploadDirectory(conf, files, options = {}) { - return await uploadBlockStream( - conf, - UnixFS.createDirectoryEncoderStream(files, options), - options - ) -} - -/** - * Uploads a CAR file to the service. - * - * The difference between this function and `Store.add` is that the CAR file is - * automatically sharded and an "upload" is registered, linking the individual - * shards (see `Upload.add`). - * - * Use the `onShardStored` callback to obtain the CIDs of the CAR file shards. - * - * Required delegated capability proofs: `store/add`, `upload/add` - * - * @param {import('./types.js').InvocationConfig} conf Configuration - * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. - * - * The `issuer` is the signing authority that is issuing the UCAN - * invocation(s). It is typically the user _agent_. - * - * The `with` is the resource the invocation applies to. It is typically the - * DID of a space. - * - * The `proofs` are a set of capability delegations that prove the issuer - * has the capability to perform the action. - * - * The issuer needs the `store/add` and `upload/add` delegated capability. - * @param {import('./types.js').BlobLike} car CAR file. - * @param {import('./types.js').UploadOptions} [options] - */ -export async function uploadCAR(conf, car, options = {}) { - const blocks = new CAR.BlockStream(car) - options.rootCID = options.rootCID ?? (await blocks.getRoots())[0] - return await uploadBlockStream(conf, blocks, options) -} - -/** - * @param {import('./types.js').InvocationConfig} conf - * @param {ReadableStream} blocks - * @param {import('./types.js').UploadOptions} [options] - * @returns {Promise} - */ -async function uploadBlockStream(conf, blocks, options = {}) { - /** @type {import('./types.js').CARLink[]} */ - const shards = [] - /** @type {import('./types.js').AnyLink?} */ - let root = null - const concurrency = options.concurrentRequests ?? CONCURRENT_REQUESTS - await blocks - .pipeThrough(new ShardingStream(options)) - .pipeThrough( - new Parallel(concurrency, async (car) => { - const bytes = new Uint8Array(await car.arrayBuffer()) - const [cid, piece] = await Promise.all([ - Store.add(conf, bytes, options), - (async () => { - const multihashDigest = await PieceHasher.digest(bytes) - return /** @type {import('@web3-storage/capabilities/types').PieceLink} */ ( - Link.create(raw.code, multihashDigest) - ) - })(), - ]) - const { version, roots, size } = car - return { version, roots, size, cid, piece } - }) - ) - .pipeTo( - new WritableStream({ - write(meta) { - root = root || meta.roots[0] - shards.push(meta.cid) - if (options.onShardStored) options.onShardStored(meta) - }, - }) - ) - - /* c8 ignore next */ - if (!root) throw new Error('missing root CID') - - await Upload.add(conf, root, shards, options) - return root -} +export * from '@web3-storage/w3up-client/capability/upload' diff --git a/packages/upload-client/src/sharding.js b/packages/upload-client/src/sharding.js index b69bdc4e0..4fb4fcf36 100644 --- a/packages/upload-client/src/sharding.js +++ b/packages/upload-client/src/sharding.js @@ -1,86 +1 @@ -import { blockEncodingLength, encode, headerEncodingLength } from './car.js' - -// https://observablehq.com/@gozala/w3up-shard-size -const SHARD_SIZE = 133_169_152 - -/** - * Shard a set of blocks into a set of CAR files. By default the last block - * received is assumed to be the DAG root and becomes the CAR root CID for the - * last CAR output. Set the `rootCID` option to override. - * - * @extends {TransformStream} - */ -export class ShardingStream extends TransformStream { - /** - * @param {import('./types.js').ShardingOptions} [options] - */ - constructor(options = {}) { - const shardSize = options.shardSize ?? SHARD_SIZE - const maxBlockLength = shardSize - headerEncodingLength() - /** @type {import('@ipld/unixfs').Block[]} */ - let blocks = [] - /** @type {import('@ipld/unixfs').Block[] | null} */ - let readyBlocks = null - let currentLength = 0 - - super({ - async transform(block, controller) { - if (readyBlocks != null) { - controller.enqueue(await encode(readyBlocks)) - readyBlocks = null - } - - const blockLength = blockEncodingLength(block) - if (blockLength > maxBlockLength) { - throw new Error( - `block will cause CAR to exceed shard size: ${block.cid}` - ) - } - - if (blocks.length && currentLength + blockLength > maxBlockLength) { - readyBlocks = blocks - blocks = [] - currentLength = 0 - } - blocks.push(block) - currentLength += blockLength - }, - - async flush(controller) { - if (readyBlocks != null) { - controller.enqueue(await encode(readyBlocks)) - } - - const rootBlock = blocks.at(-1) - if (rootBlock == null) return - - const rootCID = options.rootCID ?? rootBlock.cid - const headerLength = headerEncodingLength(rootCID) - - // if adding CAR root overflows the shard limit we move overflowing - // blocks into a another CAR. - if (headerLength + currentLength > shardSize) { - const overage = headerLength + currentLength - shardSize - const overflowBlocks = [] - let overflowCurrentLength = 0 - while (overflowCurrentLength < overage) { - const block = blocks[blocks.length - 1] - blocks.pop() - overflowBlocks.unshift(block) - overflowCurrentLength += blockEncodingLength(block) - - // need at least 1 block in original shard - if (blocks.length < 1) - throw new Error( - `block will cause CAR to exceed shard size: ${block.cid}` - ) - } - controller.enqueue(await encode(blocks)) - controller.enqueue(await encode(overflowBlocks, rootCID)) - } else { - controller.enqueue(await encode(blocks, rootCID)) - } - }, - }) - } -} +export * from '@web3-storage/w3up-client/capability/upload/sharding' diff --git a/packages/upload-client/src/store.js b/packages/upload-client/src/store.js index 6dab2a436..f1e6092c0 100644 --- a/packages/upload-client/src/store.js +++ b/packages/upload-client/src/store.js @@ -1,231 +1 @@ -import { CAR } from '@ucanto/transport' -import * as StoreCapabilities from '@web3-storage/capabilities/store' -import { SpaceDID } from '@web3-storage/capabilities/utils' -import retry, { AbortError } from 'p-retry' -import { servicePrincipal, connection } from './service.js' -import { REQUEST_RETRIES } from './constants.js' -import fetchPkg from 'ipfs-utils/src/http/fetch.js' -const { fetch } = fetchPkg - -/** - * - * @param {string} url - * @param {import('./types.js').ProgressFn} handler - */ -function createUploadProgressHandler(url, handler) { - /** - * - * @param {import('./types.js').ProgressStatus} status - */ - function onUploadProgress({ total, loaded, lengthComputable }) { - return handler({ total, loaded, lengthComputable, url }) - } - return onUploadProgress -} - -/** - * Store a DAG encoded as a CAR file. The issuer needs the `store/add` - * delegated capability. - * - * Required delegated capability proofs: `store/add` - * - * @param {import('./types.js').InvocationConfig} conf Configuration - * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. - * - * The `issuer` is the signing authority that is issuing the UCAN - * invocation(s). It is typically the user _agent_. - * - * The `with` is the resource the invocation applies to. It is typically the - * DID of a space. - * - * The `proofs` are a set of capability delegations that prove the issuer - * has the capability to perform the action. - * - * The issuer needs the `store/add` delegated capability. - * @param {Blob|Uint8Array} car CAR file data. - * @param {import('./types.js').RequestOptions} [options] - * @returns {Promise} - */ -export async function add( - { issuer, with: resource, proofs, audience }, - car, - options = {} -) { - // TODO: validate blob contains CAR data - const bytes = - car instanceof Uint8Array ? car : new Uint8Array(await car.arrayBuffer()) - const link = await CAR.codec.link(bytes) - /* c8 ignore next */ - const conn = options.connection ?? connection - const result = await retry( - async () => { - return await StoreCapabilities.add - .invoke({ - issuer, - /* c8 ignore next */ - audience: audience ?? servicePrincipal, - with: SpaceDID.from(resource), - nb: { link, size: bytes.length }, - proofs, - }) - .execute(conn) - }, - { - onFailedAttempt: console.warn, - retries: options.retries ?? REQUEST_RETRIES, - } - ) - - if (!result.out.ok) { - throw new Error(`failed ${StoreCapabilities.add.can} invocation`, { - cause: result.out.error, - }) - } - - // Return early if it was already uploaded. - if (result.out.ok.status === 'done') { - return link - } - - const responseAddUpload = result.out.ok - - const fetchWithUploadProgress = - /** @type {(url: string, init?: import('./types.js').FetchOptions) => Promise} */ ( - fetch - ) - - const res = await retry( - async () => { - try { - const res = await fetchWithUploadProgress(responseAddUpload.url, { - method: 'PUT', - mode: 'cors', - body: car, - headers: responseAddUpload.headers, - signal: options.signal, - onUploadProgress: options.onUploadProgress - ? createUploadProgressHandler( - responseAddUpload.url, - options.onUploadProgress - ) - : undefined, - // @ts-expect-error - this is needed by recent versions of node - see https://github.com/bluesky-social/atproto/pull/470 for more info - duplex: 'half', - }) - if (res.status >= 400 && res.status < 500) { - throw new AbortError(`upload failed: ${res.status}`) - } - return res - } catch (err) { - if (options.signal?.aborted === true) { - throw new AbortError('upload aborted') - } - throw err - } - }, - { - retries: options.retries ?? REQUEST_RETRIES, - } - ) - - if (!res.ok) { - throw new Error(`upload failed: ${res.status}`) - } - - return link -} - -/** - * List CAR files stored by the issuer. - * - * @param {import('./types.js').InvocationConfig} conf Configuration - * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. - * - * The `issuer` is the signing authority that is issuing the UCAN - * invocation(s). It is typically the user _agent_. - * - * The `with` is the resource the invocation applies to. It is typically the - * DID of a space. - * - * The `proofs` are a set of capability delegations that prove the issuer - * has the capability to perform the action. - * - * The issuer needs the `store/list` delegated capability. - * @param {import('./types.js').ListRequestOptions} [options] - * @returns {Promise} - */ -export async function list( - { issuer, with: resource, proofs, audience }, - options = {} -) { - /* c8 ignore next */ - const conn = options.connection ?? connection - const result = await StoreCapabilities.list - .invoke({ - issuer, - /* c8 ignore next */ - audience: audience ?? servicePrincipal, - with: SpaceDID.from(resource), - proofs, - nb: { - cursor: options.cursor, - size: options.size, - pre: options.pre, - }, - }) - .execute(conn) - - if (!result.out.ok) { - throw new Error(`failed ${StoreCapabilities.list.can} invocation`, { - cause: result.out.error, - }) - } - - return result.out.ok -} - -/** - * Remove a stored CAR file by CAR CID. - * - * @param {import('./types.js').InvocationConfig} conf Configuration - * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. - * - * The `issuer` is the signing authority that is issuing the UCAN - * invocation(s). It is typically the user _agent_. - * - * The `with` is the resource the invocation applies to. It is typically the - * DID of a space. - * - * The `proofs` are a set of capability delegations that prove the issuer - * has the capability to perform the action. - * - * The issuer needs the `store/remove` delegated capability. - * @param {import('./types.js').CARLink} link CID of CAR file to remove. - * @param {import('./types.js').RequestOptions} [options] - */ -export async function remove( - { issuer, with: resource, proofs, audience }, - link, - options = {} -) { - /* c8 ignore next */ - const conn = options.connection ?? connection - const result = await StoreCapabilities.remove - .invoke({ - issuer, - /* c8 ignore next */ - audience: audience ?? servicePrincipal, - with: SpaceDID.from(resource), - nb: { link }, - proofs, - }) - .execute(conn) - - if (!result.out.ok) { - throw new Error(`failed ${StoreCapabilities.remove.can} invocation`, { - cause: result.out.error, - }) - } - - return result.out -} +export * from '@web3-storage/w3up-client/capability/store' diff --git a/packages/upload-client/src/types.js b/packages/upload-client/src/types.js new file mode 100644 index 000000000..7f6e94e11 --- /dev/null +++ b/packages/upload-client/src/types.js @@ -0,0 +1 @@ +export * from '@web3-storage/w3up-client/capability/upload' diff --git a/packages/upload-client/src/unixfs.js b/packages/upload-client/src/unixfs.js index 3547ba71f..5e48bc917 100644 --- a/packages/upload-client/src/unixfs.js +++ b/packages/upload-client/src/unixfs.js @@ -1,176 +1 @@ -import * as UnixFS from '@ipld/unixfs' -import * as raw from 'multiformats/codecs/raw' -import { withMaxChunkSize } from '@ipld/unixfs/file/chunker/fixed' -import { withWidth } from '@ipld/unixfs/file/layout/balanced' - -const SHARD_THRESHOLD = 1000 // shard directory after > 1,000 items -const queuingStrategy = UnixFS.withCapacity() - -const settings = UnixFS.configure({ - fileChunkEncoder: raw, - smallFileEncoder: raw, - chunker: withMaxChunkSize(1024 * 1024), - fileLayout: withWidth(1024), -}) - -/** - * @param {import('./types.js').BlobLike} blob - * @returns {Promise} - */ -export async function encodeFile(blob) { - const readable = createFileEncoderStream(blob) - const blocks = await collect(readable) - // @ts-expect-error There is always a root block - return { cid: blocks.at(-1).cid, blocks } -} - -/** - * @param {import('./types.js').BlobLike} blob - * @returns {ReadableStream} - */ -export function createFileEncoderStream(blob) { - /** @type {TransformStream} */ - const { readable, writable } = new TransformStream({}, queuingStrategy) - const unixfsWriter = UnixFS.createWriter({ writable, settings }) - const fileBuilder = new UnixFSFileBuilder('', blob) - void (async () => { - await fileBuilder.finalize(unixfsWriter) - await unixfsWriter.close() - })() - return readable -} - -class UnixFSFileBuilder { - #file - - /** - * @param {string} name - * @param {import('./types.js').BlobLike} file - */ - constructor(name, file) { - this.name = name - this.#file = file - } - - /** @param {import('@ipld/unixfs').View} writer */ - async finalize(writer) { - const unixfsFileWriter = UnixFS.createFileWriter(writer) - await this.#file.stream().pipeTo( - new WritableStream({ - async write(chunk) { - await unixfsFileWriter.write(chunk) - }, - }) - ) - return await unixfsFileWriter.close() - } -} - -class UnixFSDirectoryBuilder { - #options - - /** @type {Map} */ - entries = new Map() - - /** - * @param {string} name - * @param {import('./types.js').UnixFSDirectoryEncoderOptions} [options] - */ - constructor(name, options) { - this.name = name - this.#options = options - } - - /** @param {import('@ipld/unixfs').View} writer */ - async finalize(writer) { - const dirWriter = - this.entries.size <= SHARD_THRESHOLD - ? UnixFS.createDirectoryWriter(writer) - : UnixFS.createShardedDirectoryWriter(writer) - for (const [name, entry] of this.entries) { - const link = await entry.finalize(writer) - if (this.#options?.onDirectoryEntryLink) { - // @ts-expect-error - this.#options.onDirectoryEntryLink({ name: entry.name, ...link }) - } - dirWriter.set(name, link) - } - return await dirWriter.close() - } -} - -/** - * @param {Iterable} files - * @param {import('./types.js').UnixFSDirectoryEncoderOptions} [options] - * @returns {Promise} - */ -export async function encodeDirectory(files, options) { - const readable = createDirectoryEncoderStream(files, options) - const blocks = await collect(readable) - // @ts-expect-error There is always a root block - return { cid: blocks.at(-1).cid, blocks } -} - -/** - * @param {Iterable} files - * @param {import('./types.js').UnixFSDirectoryEncoderOptions} [options] - * @returns {ReadableStream} - */ -export function createDirectoryEncoderStream(files, options) { - const rootDir = new UnixFSDirectoryBuilder('', options) - - for (const file of files) { - const path = file.name.split('/') - if (path[0] === '' || path[0] === '.') { - path.shift() - } - let dir = rootDir - for (const [i, name] of path.entries()) { - if (i === path.length - 1) { - dir.entries.set(name, new UnixFSFileBuilder(path.join('/'), file)) - break - } - let dirBuilder = dir.entries.get(name) - if (dirBuilder == null) { - const dirName = dir === rootDir ? name : `${dir.name}/${name}` - dirBuilder = new UnixFSDirectoryBuilder(dirName, options) - dir.entries.set(name, dirBuilder) - } - if (!(dirBuilder instanceof UnixFSDirectoryBuilder)) { - throw new Error(`"${file.name}" cannot be a file and a directory`) - } - dir = dirBuilder - } - } - - /** @type {TransformStream} */ - const { readable, writable } = new TransformStream({}, queuingStrategy) - const unixfsWriter = UnixFS.createWriter({ writable, settings }) - void (async () => { - const link = await rootDir.finalize(unixfsWriter) - if (options?.onDirectoryEntryLink) { - options.onDirectoryEntryLink({ name: '', ...link }) - } - await unixfsWriter.close() - })() - - return readable -} - -/** - * @template T - * @param {ReadableStream} collectable - * @returns {Promise} - */ -async function collect(collectable) { - /** @type {T[]} */ - const chunks = [] - await collectable.pipeTo( - new WritableStream({ - write(chunk) { - chunks.push(chunk) - }, - }) - ) - return chunks -} +export * from '@web3-storage/w3up-client/capability/upload/unixfs' diff --git a/packages/upload-client/src/upload.js b/packages/upload-client/src/upload.js index 28dd89e6c..7f6e94e11 100644 --- a/packages/upload-client/src/upload.js +++ b/packages/upload-client/src/upload.js @@ -1,161 +1 @@ -import * as UploadCapabilities from '@web3-storage/capabilities/upload' -import { SpaceDID } from '@web3-storage/capabilities/utils' -import retry from 'p-retry' -import { servicePrincipal, connection } from './service.js' -import { REQUEST_RETRIES } from './constants.js' - -/** - * Register an "upload" with the service. The issuer needs the `upload/add` - * delegated capability. - * - * Required delegated capability proofs: `upload/add` - * - * @param {import('./types.js').InvocationConfig} conf Configuration - * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. - * - * The `issuer` is the signing authority that is issuing the UCAN - * invocation(s). It is typically the user _agent_. - * - * The `with` is the resource the invocation applies to. It is typically the - * DID of a space. - * - * The `proofs` are a set of capability delegations that prove the issuer - * has the capability to perform the action. - * - * The issuer needs the `upload/add` delegated capability. - * @param {import('multiformats/link').UnknownLink} root Root data CID for the DAG that was stored. - * @param {import('./types.js').CARLink[]} shards CIDs of CAR files that contain the DAG. - * @param {import('./types.js').RequestOptions} [options] - * @returns {Promise} - */ -export async function add( - { issuer, with: resource, proofs, audience }, - root, - shards, - options = {} -) { - /* c8 ignore next */ - const conn = options.connection ?? connection - const result = await retry( - async () => { - return await UploadCapabilities.add - .invoke({ - issuer, - /* c8 ignore next */ - audience: audience ?? servicePrincipal, - with: SpaceDID.from(resource), - nb: { root, shards }, - proofs, - }) - .execute(conn) - }, - { - onFailedAttempt: console.warn, - retries: options.retries ?? REQUEST_RETRIES, - } - ) - - if (!result.out.ok) { - throw new Error(`failed ${UploadCapabilities.add.can} invocation`, { - cause: result.out.error, - }) - } - - return result.out.ok -} - -/** - * List uploads created by the issuer. - * - * @param {import('./types.js').InvocationConfig} conf Configuration - * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. - * - * The `issuer` is the signing authority that is issuing the UCAN - * invocation(s). It is typically the user _agent_. - * - * The `with` is the resource the invocation applies to. It is typically the - * DID of a space. - * - * The `proofs` are a set of capability delegations that prove the issuer - * has the capability to perform the action. - * - * The issuer needs the `upload/list` delegated capability. - * @param {import('./types.js').ListRequestOptions} [options] - * @returns {Promise} - */ -export async function list( - { issuer, with: resource, proofs, audience }, - options = {} -) { - /* c8 ignore next */ - const conn = options.connection ?? connection - - const result = await UploadCapabilities.list - .invoke({ - issuer, - /* c8 ignore next */ - audience: audience ?? servicePrincipal, - with: SpaceDID.from(resource), - proofs, - nb: { - cursor: options.cursor, - size: options.size, - pre: options.pre, - }, - }) - .execute(conn) - - if (!result.out.ok) { - throw new Error(`failed ${UploadCapabilities.list.can} invocation`, { - cause: result.out.error, - }) - } - - return result.out.ok -} - -/** - * Remove an upload by root data CID. - * - * @param {import('./types.js').InvocationConfig} conf Configuration - * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. - * - * The `issuer` is the signing authority that is issuing the UCAN - * invocation(s). It is typically the user _agent_. - * - * The `with` is the resource the invocation applies to. It is typically the - * DID of a space. - * - * The `proofs` are a set of capability delegations that prove the issuer - * has the capability to perform the action. - * - * The issuer needs the `upload/remove` delegated capability. - * @param {import('multiformats').UnknownLink} root Root data CID to remove. - * @param {import('./types.js').RequestOptions} [options] - */ -export async function remove( - { issuer, with: resource, proofs, audience }, - root, - options = {} -) { - /* c8 ignore next */ - const conn = options.connection ?? connection - const result = await UploadCapabilities.remove - .invoke({ - issuer, - /* c8 ignore next */ - audience: audience ?? servicePrincipal, - with: SpaceDID.from(resource), - nb: { root }, - proofs, - }) - .execute(conn) - - if (!result.out.ok) { - throw new Error(`failed ${UploadCapabilities.remove.can} invocation`, { - cause: result.out.error, - }) - } - - return result.out.ok -} +export * from '@web3-storage/w3up-client/capability/upload' diff --git a/packages/upload-client/test/fixtures.js b/packages/upload-client/test/fixtures.js deleted file mode 100644 index 50a464a0a..000000000 --- a/packages/upload-client/test/fixtures.js +++ /dev/null @@ -1,6 +0,0 @@ -import * as ed25519 from '@ucanto/principal/ed25519' - -/** did:key:z6MkrZ1r5XBFZjBU34qyD8fueMbMRkKw17BZaq2ivKFjnz2z */ -export const serviceSigner = ed25519.parse( - 'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=' -) diff --git a/packages/upload-client/test/helpers/bucket-server.js b/packages/upload-client/test/helpers/bucket-server.js deleted file mode 100644 index 9db6842fd..000000000 --- a/packages/upload-client/test/helpers/bucket-server.js +++ /dev/null @@ -1,15 +0,0 @@ -import { createServer } from 'http' - -const port = process.env.PORT ?? 9000 -const status = process.env.STATUS ? parseInt(process.env.STATUS) : 200 - -const server = createServer((req, res) => { - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Access-Control-Allow-Methods', '*') - res.setHeader('Access-Control-Allow-Headers', '*') - if (req.method === 'OPTIONS') return res.end() - res.statusCode = status - res.end() -}) - -server.listen(port, () => console.log(`Listening on :${port}`)) diff --git a/packages/upload-client/test/helpers/car.js b/packages/upload-client/test/helpers/car.js deleted file mode 100644 index 56d2b3a9b..000000000 --- a/packages/upload-client/test/helpers/car.js +++ /dev/null @@ -1,22 +0,0 @@ -import { CarWriter } from '@ipld/car' -import * as CAR from '@ucanto/transport/car' -import { toBlock } from './block.js' - -/** - * @param {Uint8Array} bytes - */ -export async function toCAR(bytes) { - const block = await toBlock(bytes) - const { writer, out } = CarWriter.create(block.cid) - writer.put(block) - writer.close() - - const chunks = [] - for await (const chunk of out) { - chunks.push(chunk) - } - const blob = new Blob(chunks) - const cid = await CAR.codec.link(new Uint8Array(await blob.arrayBuffer())) - - return Object.assign(blob, { cid, roots: [block.cid] }) -} diff --git a/packages/upload-client/test/helpers/mocks.js b/packages/upload-client/test/helpers/mocks.js deleted file mode 100644 index 555287f04..000000000 --- a/packages/upload-client/test/helpers/mocks.js +++ /dev/null @@ -1,44 +0,0 @@ -import * as Server from '@ucanto/server' - -const notImplemented = () => { - throw new Server.Failure('not implemented') -} - -/** - * @param {Partial<{ - * store: Partial - * upload: Partial - * }>} impl - */ -export function mockService(impl) { - return { - store: { - add: withCallCount(impl.store?.add ?? notImplemented), - get: withCallCount(impl.store?.get ?? notImplemented), - list: withCallCount(impl.store?.list ?? notImplemented), - remove: withCallCount(impl.store?.remove ?? notImplemented), - }, - upload: { - add: withCallCount(impl.upload?.add ?? notImplemented), - get: withCallCount(impl.upload?.get ?? notImplemented), - list: withCallCount(impl.upload?.list ?? notImplemented), - remove: withCallCount(impl.upload?.remove ?? notImplemented), - }, - } -} - -/** - * @template {Function} T - * @param {T} fn - */ -function withCallCount(fn) { - /** @param {T extends (...args: infer A) => any ? A : never} args */ - const countedFn = (...args) => { - countedFn.called = true - countedFn.callCount++ - return fn(...args) - } - countedFn.called = false - countedFn.callCount = 0 - return countedFn -} diff --git a/packages/upload-client/test/helpers/random.js b/packages/upload-client/test/helpers/random.js deleted file mode 100644 index 5cce080e8..000000000 --- a/packages/upload-client/test/helpers/random.js +++ /dev/null @@ -1,38 +0,0 @@ -import { toBlock } from './block.js' -import { toCAR } from './car.js' - -/** @param {number} size */ -export async function randomBytes(size) { - const bytes = new Uint8Array(size) - while (size) { - const chunk = new Uint8Array(Math.min(size, 65_536)) - if (!globalThis.crypto) { - try { - const { webcrypto } = await import('node:crypto') - webcrypto.getRandomValues(chunk) - } catch (err) { - throw new Error( - 'unknown environment - no global crypto and not Node.js', - { cause: err } - ) - } - } else { - crypto.getRandomValues(chunk) - } - size -= bytes.length - bytes.set(chunk, size) - } - return bytes -} - -/** @param {number} size */ -export async function randomCAR(size) { - const bytes = await randomBytes(size) - return toCAR(bytes) -} - -/** @param {number} size */ -export async function randomBlock(size) { - const bytes = await randomBytes(size) - return await toBlock(bytes) -} diff --git a/packages/upload-client/test/helpers/shims.js b/packages/upload-client/test/helpers/shims.js deleted file mode 100644 index 26b0c5c1f..000000000 --- a/packages/upload-client/test/helpers/shims.js +++ /dev/null @@ -1,10 +0,0 @@ -export class File extends Blob { - /** - * @param {BlobPart[]} blobParts - * @param {string} name - */ - constructor(blobParts, name) { - super(blobParts) - this.name = name - } -} diff --git a/packages/upload-client/test/helpers/utils.js b/packages/upload-client/test/helpers/utils.js deleted file mode 100644 index c173e7e70..000000000 --- a/packages/upload-client/test/helpers/utils.js +++ /dev/null @@ -1 +0,0 @@ -export const validateAuthorization = () => ({ ok: {} }) diff --git a/packages/upload-client/tsconfig.json b/packages/upload-client/tsconfig.json index 3cbd72f5a..0e14aa5aa 100644 --- a/packages/upload-client/tsconfig.json +++ b/packages/upload-client/tsconfig.json @@ -6,5 +6,5 @@ }, "include": ["src", "scripts", "test", "package.json"], "exclude": ["**/node_modules/**"], - "references": [{ "path": "../access-client" }, { "path": "../capabilities" }] + "references": [{ "path": "../w3up-client" }] } diff --git a/packages/w3up-client/package.json b/packages/w3up-client/package.json index 0bcb8795f..a3744e755 100644 --- a/packages/w3up-client/package.json +++ b/packages/w3up-client/package.json @@ -32,6 +32,14 @@ "types": "./dist/src/client.d.ts", "import": "./src/client.js" }, + "./agent": { + "types": "./dist/src/agent.d.ts", + "import": "./src/agent.js" + }, + "./account": { + "types": "./dist/src/account.d.ts", + "import": "./src/account.js" + }, "./capability/access": { "types": "./dist/src/capability/access.d.ts", "import": "./src/capability/access.js" @@ -48,7 +56,54 @@ "types": "./dist/src/capability/upload.d.ts", "import": "./src/capability/upload.js" }, - "./types": "./src/types.js" + "./capability/upload/sharding": { + "types": "./dist/src/capability/upload/sharding.d.ts", + "import": "./src/capability/upload/sharding.js" + }, + "./capability/upload/car": { + "types": "./dist/src/capability/upload/car.d.ts", + "import": "./src/capability/upload/car.js" + }, + "./capability/upload/unixfs": { + "types": "./dist/src/capability/upload/unixfs.d.ts", + "import": "./src/capability/upload/unixfs.js" + }, + "./capability/provider": { + "types": "./dist/src/capability/provider.d.ts", + "import": "./src/capability/provider.js" + }, + "./types": { + "types": "./dist/src/types.d.ts", + "import": "./src/types.js" + }, + "./store/conf": { + "types": "./dist/src/store/conf.d.ts", + "import": "./src/store/conf.js" + }, + "./store/indexed-db": { + "types": "./dist/src/store/indexed-db.d.ts", + "import": "./src/store/indexed-db.js" + }, + "./store/memory": { + "types": "./dist/src/store/memory.d.ts", + "import": "./src/store/memory.js" + }, + "./driver/conf": { + "types": "./dist/src/driver/conf.d.ts", + "import": "./src/driver/conf.js" + }, + "./driver/indexed-db": { + "types": "./dist/src/driver/indexed-db.d.ts", + "import": "./src/driver/indexed-db.js" + }, + "./driver/memory": { + "types": "./dist/src/driver/memory.d.ts", + "import": "./src/driver/memory.js" + }, + "./agent/encoding": { + "types": "./dist/src/agent/encoding.d.ts", + "import": "./src/agent/encoding.js" + } }, "publishConfig": { "access": "public" @@ -69,21 +124,35 @@ "test:browser": "playwright-test --runner mocha 'test/**/!(*.node).test.js'", "mock": "run-p mock:*", "mock:bucket-200": "PORT=9200 STATUS=200 node test/helpers/bucket-server.js", + "mock:bucket-401": "PORT=9400 STATUS=400 node test/helpers/bucket-server.js", + "mock:bucket-500": "PORT=9500 STATUS=500 node test/helpers/bucket-server.js", "rc": "npm version prerelease --preid rc", "docs": "npm run build && typedoc --out docs-generated", "docs:markdown": "npm run build && docusaurus generate-typedoc" }, "dependencies": { "@ipld/dag-ucan": "^3.4.0", + "@ipld/car": "^5.2.2", + "@ipld/dag-cbor": "^9.0.6", + "@ipld/unixfs": "^2.1.1", "@ucanto/client": "^9.0.0", "@ucanto/core": "^9.0.0", "@ucanto/interface": "^9.0.0", "@ucanto/principal": "^9.0.0", "@ucanto/transport": "^9.0.0", + "@scure/bip39": "^1.2.1", + "p-defer": "^4.0.0", + "conf": "11.0.2", + "uint8arrays": "^4.0.6", "@web3-storage/did-mailto": "workspace:^", - "@web3-storage/access": "workspace:^", "@web3-storage/capabilities": "workspace:^", - "@web3-storage/upload-client": "workspace:^" + "fr32-sha2-256-trunc254-padded-binary-tree-multihash": "^3.1.0", + "ipfs-utils": "^9.0.14", + "multiformats": "^12.1.2", + "p-retry": "^5.1.2", + "parallel-transform-web": "^1.0.0", + "bigint-mod-arith": "^3.1.2", + "one-webcrypto": "git://github.com/web3-storage/one-webcrypto" }, "devDependencies": { "@web3-storage/upload-api": "workspace:^", @@ -105,7 +174,11 @@ "typedoc": "^0.23.24", "typedoc-plugin-markdown": "^3.14.0", "typedoc-plugin-missing-exports": "^1.0.0", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "@types/sinon": "^10.0.19", + "sinon": "^15.0.3", + "ipfs-unixfs-exporter": "^10.0.0", + "blockstore-core": "^3.0.0" }, "eslintConfig": { "extends": [ @@ -123,7 +196,8 @@ "ignorePatterns": [ "dist", "coverage", - "src/types.js" + "src/types.js", + "src/agent/types.js" ] }, "directories": { diff --git a/packages/w3up-client/src/account.js b/packages/w3up-client/src/account.js index 97b581cfb..5156862cd 100644 --- a/packages/w3up-client/src/account.js +++ b/packages/w3up-client/src/account.js @@ -1,7 +1,7 @@ import * as API from './types.js' import * as Access from './capability/access.js' -import { Delegation, importAuthorization } from '@web3-storage/access/agent' -import { add as provision, AccountDID } from '@web3-storage/access/provider' +import { Delegation, importAuthorization } from './agent.js' +import { add as provision, AccountDID } from './capability/provider.js' import { fromEmail, toEmail } from '@web3-storage/did-mailto' export { fromEmail } @@ -75,13 +75,10 @@ export const list = ({ agent }, { account } = {}) => { */ export const login = async ({ agent }, email) => { const account = fromEmail(email) - const result = await Access.request( - { agent }, - { - account, - access: Access.accountAccess, - } - ) + const result = await Access.request(agent, { + account, + access: Access.accountAccess, + }) const { ok: access, error } = result /* c8 ignore next 2 - don't know how to test this */ diff --git a/packages/w3up-client/src/agent.js b/packages/w3up-client/src/agent.js new file mode 100644 index 000000000..7b93d4d3e --- /dev/null +++ b/packages/w3up-client/src/agent.js @@ -0,0 +1,676 @@ +import * as Client from '@ucanto/client' +import * as CAR from '@ucanto/transport/car' +import * as HTTP from '@ucanto/transport/http' +import * as ucanto from '@ucanto/core' +import * as Capabilities from '@web3-storage/capabilities/space' +import { attest } from '@web3-storage/capabilities/ucan' +import * as Space from './capability/space.js' + +import { invoke, delegate, DID, Delegation, Schema } from '@ucanto/core' +import { + isExpired, + isTooEarly, + canDelegateCapability, +} from './agent/delegation.js' +import { AgentData, getSessionProofs } from './agent/data.js' +import { UCAN } from '@web3-storage/capabilities' + +import * as API from './agent/types.js' + +export * from './types.js' +export * from './agent/delegation.js' +export { AgentData, Space, Delegation, Schema, getSessionProofs } +export * from './agent/use-cases.js' + +const HOST = 'https://up.web3.storage' +const PRINCIPAL = DID.parse('did:web:web3.storage') + +/** + * Keeps track of AgentData for all Agents constructed. + * Used by addSpacesFromDelegations - so it can only accept Agent as param, but + * still mutate corresponding AgentData + * + * @deprecated - remove this when deprecated addSpacesFromDelegations is removed + */ +/** @type {WeakMap>, AgentData>} */ +const agentToData = new WeakMap() + +/** + * @typedef {API.Service} Service + * @typedef {API.Receipt} Receipt + */ + +/** + * Creates a Ucanto connection for the w3access API + * + * Usage: + * + * ```js + * import { connection } from '@web3-storage/access/agent' + * ``` + * + * @template {API.DID} T - DID method + * @template {Record} [S=Service] + * @param {object} [options] + * @param {API.Principal} [options.principal] - w3access API Principal + * @param {URL} [options.url] - w3access API URL + * @param {API.Transport.Channel} [options.channel] - Ucanto channel to use + * @param {typeof fetch} [options.fetch] - Fetch implementation to use + * @returns {API.ConnectionView} + */ +export function connection(options = {}) { + return Client.connect({ + id: options.principal ?? PRINCIPAL, + codec: CAR.outbound, + channel: + options.channel ?? + HTTP.open({ + /* c8 ignore next */ + url: options.url ?? new URL(HOST), + method: 'POST', + fetch: options.fetch ?? globalThis.fetch.bind(globalThis), + }), + }) +} + +/** + * Agent + * + * Usage: + * + * ```js + * import { Agent } from '@web3-storage/access/agent' + * ``` + * + * @template {Record} [S=Service] + */ +export class Agent { + /** @type {import('./agent/data.js').AgentData} */ + #data + + /** + * @param {import('./agent/data.js').AgentData} data - Agent data + * @param {import('./agent/types.js').AgentOptions} [options] + */ + constructor(data, options = {}) { + /** @type { Client.Channel & { url?: URL } | undefined } */ + const channel = options.connection?.channel + this.url = options.url ?? channel?.url ?? new URL(HOST) + this.connection = + options.connection ?? + connection({ + principal: options.servicePrincipal, + url: this.url, + }) + this.#data = data + agentToData.set(this, this.#data) + } + + /** + * Create a new Agent instance, optionally with the passed initialization data. + * + * @template {Record} [R=Service] + * @param {Partial} [init] + * @param {API.AgentOptions & API.AgentDataOptions} [options] + */ + static async create(init, options = {}) { + const data = await AgentData.create(init, options) + return new Agent(data, options) + } + + /** + * Instantiate an Agent from pre-exported agent data. + * + * @template {Record} [R=Service] + * @param {import('./types.js').AgentDataExport} raw + * @param {import('./agent/types.js').AgentOptions & import('./agent/types.js').AgentDataOptions} [options] + */ + /* c8 ignore next 4 */ + static from(raw, options = {}) { + const data = AgentData.fromExport(raw, options) + return new Agent(data, options) + } + + get issuer() { + return this.#data.principal + } + + get meta() { + return this.#data.meta + } + + get spaces() { + return this.#data.spaces + } + + did() { + return this.#data.principal.did() + } + + /** + * Add a proof to the agent store. + * + * @param {API.Delegation} delegation + */ + async addProof(delegation) { + return await this.addProofs([delegation]) + } + + /** + * Adds set of proofs to the agent store. + * + * @param {Iterable} delegations + */ + async addProofs(delegations) { + for (const proof of delegations) { + await this.#data.addDelegation(proof, { audience: this.meta }) + } + await this.removeExpiredDelegations() + + return {} + } + + /** + * Query the delegations store for all the delegations matching the capabilities provided. + * + * @param {API.CapabilityQuery[]} [caps] + */ + #delegations(caps) { + const _caps = new Set(caps) + /** @type {Array<{ delegation: API.Delegation, meta: API.DelegationMeta }>} */ + const values = [] + for (const [, value] of this.#data.delegations) { + // check expiration + if ( + !isExpired(value.delegation) && // check if delegation can be used + !isTooEarly(value.delegation) + ) { + // check if we need to filter for caps + if (Array.isArray(caps) && caps.length > 0) { + for (const cap of _caps) { + if (canDelegateCapability(value.delegation, cap)) { + values.push(value) + } + } + } else { + values.push(value) + } + } + } + return values + } + + /** + * Clean up any expired delegations. + */ + async removeExpiredDelegations() { + for (const [, value] of this.#data.delegations) { + /* c8 ignore next 3 */ + if (isExpired(value.delegation)) { + await this.#data.removeDelegation(value.delegation.cid) + } + } + } + + /** + * Revoke a delegation by CID. + * + * If the delegation was issued by this agent (and therefore is stored in the + * delegation store) you can just pass the CID. If not, or if the current agent's + * delegation store no longer contains the delegation, you MUST pass a chain of + * proofs that proves your authority to revoke this delegation as `options.proofs`. + * + * @param {API.UCANLink} delegationCID + * @param {object} [options] + * @param {API.Delegation[]} [options.proofs] + */ + async revoke(delegationCID, options = {}) { + const additionalProofs = options.proofs ?? [] + // look for the identified delegation in the delegation store and the passed proofs + const delegation = [...this.delegations(), ...additionalProofs].find( + (delegation) => delegation.cid.equals(delegationCID) + ) + if (!delegation) { + return { + error: new Error( + `could not find delegation ${delegationCID.toString()} - please include the delegation in options.proofs` + ), + } + } + const receipt = await this.invokeAndExecute(UCAN.revoke, { + // per https://github.com/web3-storage/w3up/blob/main/packages/capabilities/src/ucan.js#L38C6-L38C6 the resource here should be + // the current issuer - using the space DID here works for simple cases but falls apart when a delegee tries to revoke a delegation + // they have re-delegated, since they don't have "ucan/revoke" capabilities on the space + with: this.issuer.did(), + nb: { + ucan: delegation.cid, + }, + proofs: [delegation, ...additionalProofs], + }) + return receipt.out + } + + /** + * Get all the proofs matching the capabilities. + * + * Proofs are delegations with an audience matching agent DID, or with an + * audience matching the session DID. + * + * Proof of session will also be included in the returned proofs if any + * proofs matching the passed capabilities require it. + * + * @param {API.CapabilityQuery[]} [caps] - Capabilities to filter by. Empty or undefined caps with return all the proofs. + * @param {object} [options] + * @param {API.DID} [options.sessionProofIssuer] - only include session proofs for this issuer + */ + proofs(caps, options) { + const authorizations = [] + for (const { delegation } of this.#delegations(caps)) { + if (delegation.audience.did() === this.issuer.did()) { + authorizations.push(delegation) + } + } + + // now let's add any session proofs that refer to those authorizations + const sessions = getSessionProofs(this.#data) + for (const proof of authorizations) { + const proofsByIssuer = sessions[proof.asCID.toString()] ?? {} + const sessionProofs = options?.sessionProofIssuer + ? proofsByIssuer[options.sessionProofIssuer] ?? [] + : Object.values(proofsByIssuer).flat() + if (sessionProofs.length) { + authorizations.push(...sessionProofs) + } + } + return authorizations + } + + /** + * Get delegations created by the agent for others. + * + * @param {API.CapabilityQuery[]} [caps] - Capabilities to filter by. Empty or undefined caps with return all the delegations. + */ + delegations(caps) { + const arr = [] + + for (const { delegation } of this.delegationsWithMeta(caps)) { + arr.push(delegation) + } + + return arr + } + + /** + * Get delegations created by the agent for others and their metadata. + * + * @param {API.CapabilityQuery[]} [caps] - Capabilities to filter by. Empty or undefined caps with return all the delegations. + */ + delegationsWithMeta(caps) { + const arr = [] + + for (const value of this.#delegations(caps)) { + const { delegation } = value + const isSession = delegation.capabilities.some( + (c) => c.can === attest.can + ) + if (!isSession && delegation.audience.did() !== this.issuer.did()) { + arr.push(value) + } + } + + return arr + } + + /** + * Creates a space signer and a delegation to the agent + * + * @param {string} name + */ + async createSpace(name) { + return await Space.generate({ name }) + } + + /** + * Import a space from a delegation. + * + * @param {API.Delegation} delegation + * @param {object} options + * @param {string} [options.name] + */ + async importSpaceFromDelegation(delegation, { name = '' } = {}) { + const space = + name === '' + ? Space.fromDelegation(delegation) + : /* c8 ignore next */ + Space.fromDelegation(delegation).withName(name) + + /* c8 ignore next 5 */ + if (space.name === '') { + throw new Error( + 'Space has no name, please pass a `name` option to specify it' + ) + } + + this.#data.spaces.set(space.did(), { ...space.meta, name: space.name }) + + await this.addProof(space.delegation) + + // if we do not have a current space, make this one current + if (!this.currentSpace()) { + await this.setCurrentSpace(space.did()) + } + + return space + } + + /** + * Sets the current selected space + * + * Other methods will default to use the current space if no resource is defined + * + * @param {API.SpaceDID} space + */ + async setCurrentSpace(space) { + if (!this.#data.spaces.has(space)) { + throw new Error(`Agent has no proofs for ${space}.`) + } + + await this.#data.setCurrentSpace(space) + + return space + } + + /** + * Get current space DID + */ + currentSpace() { + return this.#data.currentSpace + } + + /** + * Get current space DID, proofs and abilities + */ + currentSpaceWithMeta() { + /* c8 ignore next 3 */ + if (!this.#data.currentSpace) { + return + } + + const proofs = this.proofs([ + { + can: 'space/info', + with: this.#data.currentSpace, + }, + ]) + + const caps = new Set() + for (const p of proofs) { + for (const cap of p.capabilities) { + caps.add(cap.can) + } + } + + return { + did: this.#data.currentSpace, + proofs, + capabilities: [...caps], + meta: this.#data.spaces.get(this.#data.currentSpace), + } + } + + /** + * + * @param {API.DelegationOptions} options + */ + async delegate(options) { + const space = this.currentSpaceWithMeta() + /* c8 ignore next 3 */ + if (!space) { + throw new Error('no space selected.') + } + + const caps = /** @type {API.Capabilities} */ ( + options.abilities.map((a) => { + return { + with: space.did, + can: a, + } + }) + ) + + // Verify agent can provide proofs for each requested capability + for (const cap of caps) { + if (!this.proofs([cap]).length) { + throw new Error( + `cannot delegate capability ${cap.can} with ${cap.with}` + ) + } + } + + const delegation = await delegate({ + issuer: this.issuer, + capabilities: caps, + proofs: this.proofs(caps), + /* c8 ignore next */ + facts: [{ space: space.meta ?? {} }], + ...options, + }) + + await this.#data.addDelegation(delegation, { + audience: options.audienceMeta, + }) + await this.removeExpiredDelegations() + + return delegation + } + + /** + * Invoke and execute the given capability on the Access service connection + * + * ```js + * + * await agent.invokeAndExecute(Space.recover, { + * nb: { + * identity: 'mailto: email@gmail.com', + * }, + * }) + * + * // sugar for + * const recoverInvocation = await agent.invoke(Space.recover, { + * nb: { + * identity: 'mailto: email@gmail.com', + * }, + * }) + * + * await recoverInvocation.execute(agent.connection) + * ``` + * + * @template {API.Ability} A + * @template {API.URI} R + * @template {API.Caveats} C + * @param {API.TheCapabilityParser>} cap + * @param {API.InvokeOptions>>} options + * @returns {Promise, S>>} + */ + async invokeAndExecute(cap, options) { + const inv = await this.invoke(cap, options) + const out = inv.execute(/** @type {*} */ (this.connection)) + return /** @type {*} */ (out) + } + + /** + * Execute invocations on the agent's connection + * + * @example + * ```js + * const i1 = await agent.invoke(Space.info, {}) + * const i2 = await agent.invoke(Space.recover, { + * nb: { + * identity: 'mailto:hello@web3.storage', + * }, + * }) + * + * const results = await agent.execute2(i1, i2) + * + * ``` + * @template {API.Capability} C + * @template {API.Tuple>} I + * @param {I} invocations + */ + execute(...invocations) { + return this.connection.execute(...invocations) + } + + /** + * Creates an invocation for the given capability with Agent's proofs, service, issuer and space. + * + * @example + * ```js + * const recoverInvocation = await agent.invoke(Space.recover, { + * nb: { + * identity: 'mailto: email@gmail.com', + * }, + * }) + * + * await recoverInvocation.execute(agent.connection) + * // or + * await agent.execute(recoverInvocation) + * ``` + * + * @template {API.Ability} A + * @template {API.URI} R + * @template {API.TheCapabilityParser>} CAP + * @template {API.Caveats} [C={}] + * @param {CAP} cap + * @param {import('./agent/types.js').InvokeOptions} options + */ + async invoke(cap, options) { + const audience = options.audience || this.connection.id + + const space = options.with || this.currentSpace() + /* c8 ignore next 5 */ + if (!space) { + throw new Error( + 'No space or resource selected, you need pass a resource.' + ) + } + + const proofs = [ + ...(options.proofs || []), + ...this.proofs( + [ + { + with: space, + can: cap.can, + }, + ], + { sessionProofIssuer: audience.did() } + ), + ] + + if (proofs.length === 0 && options.with !== this.did()) { + throw new Error( + `no proofs available for resource ${space} and ability ${cap.can}` + ) + } + const inv = invoke({ + ...options, + audience, + // @ts-ignore + capability: cap.create({ + with: space, + nb: options.nb, + }), + issuer: this.issuer, + proofs: [...proofs], + }) + + return /** @type {API.IssuedInvocationView>} */ ( + inv + ) + } + + /** + * Get Space information from Access service + * + * @param {API.URI<"did:">} [space] + */ + async getSpaceInfo(space) { + const _space = space || this.currentSpace() + /* c8 ignore next 3 */ + if (!_space) { + throw new Error('No space selected, you need pass a resource.') + } + const inv = await this.invokeAndExecute(Capabilities.info, { + with: _space, + }) + + /* c8 ignore next 3 */ + if (inv.out.error) { + throw inv.out.error + } + + return /** @type {import('./agent/types.js').SpaceInfoResult} */ ( + inv.out.ok + ) + } +} + +/** + * Given a list of delegations, add to agent data spaces list. + * + * @deprecated - trying to remove explicit space tracking from Agent/AgentData + * in favor of functions that derive the space set from access.delegations + * + * @template {Record} [S=Service] + * @param {Agent} agent + * @param {API.Delegation[]} delegations + */ +export async function addSpacesFromDelegations(agent, delegations) { + const data = agentToData.get(agent) + /* c8 ignore next 5 */ + if (!data) { + throw Object.assign(new Error(`cannot determine AgentData for Agent`), { + agent: agent, + }) + } + + for (const delegation of delegations) { + // We only consider delegations to this agent as those are only spaces that + // this agent will be able to interact with. + if (delegation.audience.did() === agent.did()) { + // TODO: we need a more robust way to determine which spaces a user has access to + // it may or may not involve look at delegations + const allows = ucanto.Delegation.allows(delegation) + + for (const [did, value] of Object.entries(allows)) { + // If we discovered a delegation to any DID, we add it to the spaces list. + if (did.startsWith('did:key') && Object.keys(value).length > 0) { + data.addSpace(/** @type {API.DID} */ (did), { + name: '', + }) + } + } + } + } +} + +/** + * Stores given delegations in the agent's data store and adds discovered spaces + * to the agent's space list. + * + * @param {Agent} agent + * @param {object} authorization + * @param {API.Delegation[]} authorization.proofs + * @returns {Promise>} + */ +export const importAuthorization = async (agent, { proofs }) => { + try { + await agent.addProofs(proofs) + await addSpacesFromDelegations(agent, proofs) + return { ok: {} } + /* c8 ignore next 3 */ + } catch (error) { + return /** @type {{error:Error}} */ ({ error }) + } +} diff --git a/packages/access-client/src/agent-data.js b/packages/w3up-client/src/agent/data.js similarity index 97% rename from packages/access-client/src/agent-data.js rename to packages/w3up-client/src/agent/data.js index a03bb680d..c0e220c26 100644 --- a/packages/access-client/src/agent-data.js +++ b/packages/w3up-client/src/agent/data.js @@ -4,7 +4,7 @@ import { importDAG } from '@ucanto/core/delegation' import * as Ucanto from '@ucanto/interface' import { CID } from 'multiformats' import { UCAN } from '@web3-storage/capabilities' -import { isExpired } from './delegations.js' +import { isExpired } from './delegation.js' /** @typedef {import('./types.js').AgentDataModel} AgentDataModel */ @@ -32,6 +32,7 @@ export class AgentData { * * @param {Partial} [init] * @param {import('./types.js').AgentDataOptions} [options] + * @returns {Promise} */ static async create(init = {}, options = {}) { const agentData = new AgentData( @@ -117,6 +118,7 @@ export class AgentData { */ async addSpace(did, meta, proof) { this.spaces.set(did, meta) + /* c8 ignore next 1 */ await (proof ? this.addDelegation(proof) : this.#save(this.export())) } @@ -144,6 +146,7 @@ export class AgentData { /** * @param {import('@ucanto/interface').UCANLink} cid */ + /* c8 ignore next 4 */ async removeDelegation(cid) { this.delegations.delete(cid.toString()) await this.#save(this.export()) diff --git a/packages/access-client/src/delegations.js b/packages/w3up-client/src/agent/delegation.js similarity index 96% rename from packages/access-client/src/delegations.js rename to packages/w3up-client/src/agent/delegation.js index 7cda291f6..070349626 100644 --- a/packages/access-client/src/delegations.js +++ b/packages/w3up-client/src/agent/delegation.js @@ -7,6 +7,7 @@ import { canDelegateAbility } from '@web3-storage/capabilities/utils' * @param {API.Delegation} delegation */ export function isExpired(delegation) { + /* c8 ignore next 6 */ if ( delegation.expiration === undefined || delegation.expiration <= Math.floor(Date.now() / 1000) @@ -24,6 +25,7 @@ export function isTooEarly(delegation) { if (!delegation.notBefore) { return false } + /* c8 ignore next */ return delegation.notBefore > Math.floor(Date.now() / 1000) } @@ -35,6 +37,7 @@ export function isTooEarly(delegation) { * @param {boolean} [opts.checkIsExpired] * @param {boolean} [opts.checkIsTooEarly] */ +/* c8 ignore next 21 */ export function validate(delegation, opts) { const { checkAudience, @@ -87,6 +90,7 @@ export function canDelegateCapability(delegation, capability) { * @param {API.ResourceQuery} query */ export const matchResource = (resource, query) => { + /* c8 ignore next 2 */ if (query === 'ucan:*') { return true } else if (typeof query === 'string') { diff --git a/packages/w3up-client/src/agent/encoding.js b/packages/w3up-client/src/agent/encoding.js new file mode 100644 index 000000000..ce6dde1fa --- /dev/null +++ b/packages/w3up-client/src/agent/encoding.js @@ -0,0 +1,158 @@ +/** + * Encoding utilities + * + * It is recommended that you import directly with: + * ```js + * import * as Encoding from '@web3-storage/access/encoding' + * + * // or + * + * import { encodeDelegations } from '@web3-storage/access/encoding' + * ``` + * + * @module + */ +import { CarBufferReader } from '@ipld/car/buffer-reader' +import * as CarBufferWriter from '@ipld/car/buffer-writer' +import { Delegation } from '@ucanto/core/delegation' +import * as u8 from 'uint8arrays' +// eslint-disable-next-line no-unused-vars +import * as Types from '@ucanto/interface' + +/** + * Encode delegations as bytes + * + * @param {Types.Delegation[]} delegations + */ +export function delegationsToBytes(delegations) { + if (!Array.isArray(delegations) || delegations.length === 0) { + throw new Error('Delegations required to be an non empty array.') + } + + const roots = delegations.map( + (d) => /** @type {CarBufferWriter.CID} */ (d.root.cid) + ) + const cids = new Set() + /** @type {CarBufferWriter.Block[]} */ + const blocks = [] + let byteLength = 0 + + for (const delegation of delegations) { + for (const block of delegation.export()) { + const cid = block.cid.toV1().toString() + if (!cids.has(cid)) { + byteLength += CarBufferWriter.blockLength( + /** @type {CarBufferWriter.Block} */ (block) + ) + blocks.push(/** @type {CarBufferWriter.Block} */ (block)) + cids.add(cid) + } + } + } + const headerLength = CarBufferWriter.estimateHeaderLength(roots.length) + const writer = CarBufferWriter.createWriter( + new ArrayBuffer(headerLength + byteLength), + { roots } + ) + for (const block of blocks) { + writer.write(block) + } + + return writer.close() +} + +/** + * Decode bytes into Delegations + * + * @template {Types.Capabilities} [T=Types.Capabilities] + * @param {import('./types.js').BytesDelegation} bytes + */ +export function bytesToDelegations(bytes) { + if (!(bytes instanceof Uint8Array) || bytes.length === 0) { + throw new TypeError('Input should be a non-empty Uint8Array.') + } + const reader = CarBufferReader.fromBytes(bytes) + const roots = reader.getRoots() + + /** @type {Types.Delegation[]} */ + const delegations = [] + + for (const root of roots) { + const rootBlock = reader.get(root) + + if (rootBlock) { + const blocks = new Map() + for (const block of reader.blocks()) { + if (block.cid.toString() !== root.toString()) + blocks.set(block.cid.toString(), block) + } + + // @ts-ignore + delegations.push(new Delegation(rootBlock, blocks)) + /* c8 ignore next 3 */ + } else { + throw new Error('Failed to find root from raw delegation.') + } + } + + return delegations +} + +/** + * @param {Types.Delegation[]} delegations + * @param {import('uint8arrays/to-string').SupportedEncodings} encoding + */ +export function delegationsToString(delegations, encoding = 'base64url') { + const bytes = delegationsToBytes(delegations) + + return u8.toString(bytes, encoding) +} + +/** + * Encode one {@link Types.Delegation Delegation} into a string + * + * @param {Types.Delegation} delegation + * @param {import('uint8arrays/to-string').SupportedEncodings} [encoding] + */ +export function delegationToString(delegation, encoding) { + return delegationsToString([delegation], encoding) +} + +/** + * Decode string into {@link Types.Delegation Delegation} + * + * @template {Types.Capabilities} [T=Types.Capabilities] + * @param {import('./types.js').EncodedDelegation} raw + * @param {import('uint8arrays/to-string').SupportedEncodings} [encoding] + */ +export function stringToDelegations(raw, encoding = 'base64url') { + const bytes = u8.fromString(raw, encoding) + + return bytesToDelegations(bytes) +} + +/** + * Decode string into a {@link Types.Delegation Delegation} + * + * @template {Types.Capabilities} [T=Types.Capabilities] + * @param {import('./types.js').EncodedDelegation} raw + * @param {import('uint8arrays/to-string').SupportedEncodings} [encoding] + */ +export function stringToDelegation(raw, encoding) { + const delegations = stringToDelegations(raw, encoding) + + return /** @type {Types.Delegation} */ (delegations[0]) +} + +/** + * @param {number} [expiration] + */ +/* c8 ignore next 8 */ +export function expirationToDate(expiration) { + const expires = + expiration === Infinity || !expiration + ? undefined + : new Date(expiration * 1000) + + return expires +} diff --git a/packages/access-client/src/errors.ts b/packages/w3up-client/src/agent/errors.ts similarity index 100% rename from packages/access-client/src/errors.ts rename to packages/w3up-client/src/agent/errors.ts diff --git a/packages/w3up-client/src/agent/types.js b/packages/w3up-client/src/agent/types.js new file mode 100644 index 000000000..336ce12bb --- /dev/null +++ b/packages/w3up-client/src/agent/types.js @@ -0,0 +1 @@ +export {} diff --git a/packages/access-client/src/types.ts b/packages/w3up-client/src/agent/types.ts similarity index 96% rename from packages/access-client/src/types.ts rename to packages/w3up-client/src/agent/types.ts index 044735731..d4406ec91 100644 --- a/packages/access-client/src/types.ts +++ b/packages/w3up-client/src/agent/types.ts @@ -53,8 +53,7 @@ import type { PlanGetSuccess, PlanGetFailure, } from '@web3-storage/capabilities/types' -import type { SetRequired } from 'type-fest' -import { Driver } from './drivers/types.js' +import { Driver } from '../driver/types.js' import { SpaceUnknown } from './errors.js' // export other types @@ -62,7 +61,13 @@ export * from '@ucanto/interface' export * from '@web3-storage/capabilities/types' export * from './errors.js' export * from '@web3-storage/did-mailto' -export type { Agent } from './agent.js' +export type { Agent } from '../agent.js' + +export type { + UCANRevoke, + UCANRevokeSuccess, + UCANRevokeFailure, +} from '@web3-storage/capabilities/types' export interface SpaceInfoResult { // space did @@ -231,7 +236,9 @@ export type InvokeOptions< proofs?: Delegation[] } -export type DelegationOptions = SetRequired & { +export type DelegationOptions = UCANBasicOptions & { + audience: Principal + /** * Abilities to delegate */ diff --git a/packages/access-client/src/agent-use-cases.js b/packages/w3up-client/src/agent/use-cases.js similarity index 94% rename from packages/access-client/src/agent-use-cases.js rename to packages/w3up-client/src/agent/use-cases.js index c32f2f2c5..445a18007 100644 --- a/packages/access-client/src/agent-use-cases.js +++ b/packages/w3up-client/src/agent/use-cases.js @@ -1,12 +1,13 @@ -import { addSpacesFromDelegations, Agent as AccessAgent } from './agent.js' +import { addSpacesFromDelegations, Agent as AccessAgent } from '../agent.js' import * as Access from '@web3-storage/capabilities/access' import { bytesToDelegations } from './encoding.js' import { Provider, Plan } from '@web3-storage/capabilities' import * as w3caps from '@web3-storage/capabilities' import { Schema, delegate } from '@ucanto/core' -import { AgentData, isSessionProof } from './agent-data.js' +import { AgentData, isSessionProof } from './data.js' import * as DidMailto from '@web3-storage/did-mailto' import * as API from './types.js' +import * as Result from '../result.js' const DIDWeb = Schema.DID.match({ method: 'web' }) @@ -27,9 +28,8 @@ export async function requestAccess(access, account, capabilities) { att: [...capabilities], }, }) - if (res?.out.error) { - throw res.out.error - } + + return Result.try(res.out) } /** @@ -50,11 +50,9 @@ export async function claimAccess( audience: access.connection.id, with: audienceOfClaimedDelegations, }) - if (res.out.error) { - throw res.out.error - } - const delegations = Object.values(res.out.ok.delegations).flatMap((bytes) => - bytesToDelegations(bytes) + + const delegations = Object.values(Result.try(res.out).delegations).flatMap( + (bytes) => bytesToDelegations(bytes) ) if (addProofs) { for (const d of delegations) { @@ -74,6 +72,7 @@ export async function claimAccess( * @param {API.Principal} opts.account * @param {API.ProviderDID} opts.provider - e.g. 'did:web:staging.web3.storage' */ +/* c8 ignore next 12 */ export async function addProvider({ access, space, account, provider }) { const result = await access.invokeAndExecute(Provider.add, { audience: access.connection.id, @@ -83,9 +82,8 @@ export async function addProvider({ access, space, account, provider }) { consumer: space, }, }) - if (result.out.error) { - throw result.out.error - } + + return Result.try(result.out) } /** @@ -118,12 +116,12 @@ export async function pollAccessClaimUntil( // eslint-disable-next-line no-constant-condition while (true) { if (opts?.signal?.aborted) + /* c8 ignore next */ throw opts.signal.reason ?? new Error('operation aborted') const res = await access.invokeAndExecute(w3caps.Access.claim, { with: delegee, }) - if (res.out.error) throw res.out.error - const claims = Object.values(res.out.ok.delegations).flatMap((d) => + const claims = Object.values(Result.try(res.out).delegations).flatMap((d) => bytesToDelegations(d) ) if (delegationsMatch(claims)) return claims @@ -210,6 +208,7 @@ export async function authorizeAndWait(access, email, opts = {}) { export async function authorizeWaitAndClaim(accessAgent, email, opts) { await authorizeAndWait(accessAgent, email, opts) await claimAccess(accessAgent, accessAgent.issuer.did(), { + /* c8 ignore next */ addProofs: opts?.addProofs ?? true, }) } @@ -227,6 +226,7 @@ export async function authorizeWaitAndClaim(accessAgent, email, opts) { * @param {API.DID<'key'>} [opts.space] * @param {API.ProviderDID} [opts.provider] - provider to register - defaults to this.connection.id */ +/* c8 ignore next 39 */ export async function addProviderAndDelegateToAccount( access, agentData, @@ -266,6 +266,7 @@ export async function addProviderAndDelegateToAccount( throw delegateSpaceAccessResult.out.error } + /* c8 ignore next 2 */ await agentData.addSpace(space, spaceMeta) } @@ -274,6 +275,7 @@ export async function addProviderAndDelegateToAccount( * @param {API.SpaceDID} space * @param {API.Principal} account */ +/* c8 ignore next 26 */ async function delegateSpaceAccessToAccount(access, space, account) { const issuerSaysAccountCanAdminSpace = await createIssuerSaysAccountCanAdminSpace( @@ -300,6 +302,7 @@ async function delegateSpaceAccessToAccount(access, space, account) { issuerSaysAccountCanAdminSpace, ], }) + /* c8 ignore next */ } /** @@ -311,6 +314,7 @@ async function delegateSpaceAccessToAccount(access, space, account) { * @param {number} expiration * @returns */ +/* c8 ignore next 20 */ async function createIssuerSaysAccountCanAdminSpace( issuer, space, @@ -331,6 +335,7 @@ async function createIssuerSaysAccountCanAdminSpace( proofs, expiration, }) + /* c8 ignore next */ } /** diff --git a/packages/access-client/src/utils/json.js b/packages/w3up-client/src/agent/utils/json.js similarity index 95% rename from packages/access-client/src/utils/json.js rename to packages/w3up-client/src/agent/utils/json.js index 588c49645..e90e3c8e5 100644 --- a/packages/access-client/src/utils/json.js +++ b/packages/w3up-client/src/agent/utils/json.js @@ -5,6 +5,7 @@ * @param {any} v */ export const replacer = (k, v) => { + /* c8 ignore next 2 */ if (v instanceof URL) { return { $url: v.toString() } } else if (v instanceof Map) { @@ -23,6 +24,7 @@ export const replacer = (k, v) => { */ export const reviver = (k, v) => { if (!v) return v + /* c8 ignore next */ if (v.$url) return new URL(v.$url) if (v.$map) return new Map(v.$map) if (v.$bytes) return new Uint8Array(v.$bytes) diff --git a/packages/w3up-client/src/base.js b/packages/w3up-client/src/base.js index e49b0ffa4..fd12cdf2c 100644 --- a/packages/w3up-client/src/base.js +++ b/packages/w3up-client/src/base.js @@ -1,4 +1,4 @@ -import { Agent } from '@web3-storage/access/agent' +import { Agent } from './agent.js' import { serviceConf } from './service.js' export class Base { @@ -15,7 +15,7 @@ export class Base { _serviceConf /** - * @param {import('@web3-storage/access').AgentData} agentData + * @param {import('./agent.js').AgentData} agentData * @param {object} [options] * @param {import('./types.js').ServiceConf} [options.serviceConf] */ diff --git a/packages/w3up-client/src/capability/access.js b/packages/w3up-client/src/capability/access.js index ad97c99be..4d2cb537d 100644 --- a/packages/w3up-client/src/capability/access.js +++ b/packages/w3up-client/src/capability/access.js @@ -1,9 +1,11 @@ import { Base } from '../base.js' -import * as Agent from '@web3-storage/access/agent' +import * as Access from '@web3-storage/capabilities/access' import * as DIDMailto from '@web3-storage/did-mailto' import * as Result from '../result.js' - -import * as API from '../types.js' +import { Failure, DID } from '@ucanto/core' +import { bytesToDelegations } from '../agent/encoding.js' +import { importAuthorization } from '../agent.js' +import * as API from '../agent/types.js' export { DIDMailto } @@ -25,7 +27,7 @@ export class AccessClient extends Base { */ async authorize(email, options) { const account = DIDMailto.fromEmail(email) - const authorization = Result.unwrap(await request(this, { account })) + const authorization = Result.unwrap(await request(this.agent, { account })) const access = Result.unwrap(await authorization.claim(options)) await Result.unwrap(await access.save()) @@ -40,7 +42,7 @@ export class AccessClient extends Base { * @param {API.DID} [input.audience] */ async claim(input) { - const access = Result.unwrap(await claim(this, input)) + const access = Result.unwrap(await claim(this.agent, input)) await Result.unwrap(await access.save()) return access.proofs } @@ -54,7 +56,7 @@ export class AccessClient extends Base { * @param {AbortSignal} [input.signal] */ async request(input) { - return await request(this, input) + return await request(this.agent, input) } /** @@ -66,41 +68,358 @@ export class AccessClient extends Base { * @param {API.Delegation[]} [input.proofs] */ async delegate(input) { - return await delegate(this, input) + return await delegate(this.agent, input) } } /** - * @param {{agent: API.Agent}} client - * @param {object} [input] - * @param {API.DID} [input.audience] + * Takes array of delegations and propagates them to their respective audiences + * through a given space (or the current space if none is provided). + * + * Returns error result if agent has no current space and no space was provided. + * Also returns error result if invocation fails. + * + * @param {API.Agent} agent - Agent connected to the w3up service. + * @param {object} input + * @param {API.Delegation[]} input.delegations - Delegations to propagate. + * @param {API.SpaceDID} [input.space] - Space to propagate through. + * @param {API.Delegation[]} [input.proofs] - Optional set of proofs to be + * included in the invocation. */ -export const claim = async ({ agent }, input) => - Agent.Access.claim(agent, input) +export const delegate = async ( + agent, + { delegations, proofs = [], space = agent.currentSpace() } +) => { + /* c8 ignore next 3 */ + if (!space) { + return Result.fail('Space must be specified') + } + + const entries = Object.values(delegations).map((proof) => [ + proof.cid.toString(), + proof.cid, + ]) + + const { out } = await agent.invokeAndExecute(Access.delegate, { + with: space, + nb: { + delegations: Object.fromEntries(entries), + }, + // must be embedded here because it's referenced by cid in .nb.delegations + proofs: [...delegations, ...proofs], + }) + + return out +} /** - * Requests specified `access` level from specified `account`. It will invoke - * `access/authorize` capability and keep polling `access/claim` capability - * until access is granted or request is aborted. + * Requests specified `access` level from specified `account`. It invokes + * `access/authorize` capability, if invocation succeeds it will return a + * `PendingAccessRequest` object that can be used to poll for the requested + * delegation through `access/claim` capability. * - * @param {{agent: API.Agent}} agent + * @param {API.Agent} agent * @param {object} input - * @param {API.AccountDID} input.account - * @param {API.Access} [input.access] - * @param {API.DID} [input.audience] + * @param {API.AccountDID} input.account - Account from which access is requested. + * @param {API.ProviderDID} [input.provider] - Provider that will receive the invocation. + * @param {API.DID} [input.audience] - Principal requesting an access. + * @param {API.Access} [input.access] - Access been requested. + * @returns {Promise>} */ -export const request = async ({ agent }, input) => - Agent.Access.request(agent, input) +export const request = async ( + agent, + { + account, + provider = /** @type {API.ProviderDID} */ (agent.connection.id.did()), + audience: audience = agent.did(), + access = spaceAccess, + } +) => { + // Request access from the account. + const { out: result } = await agent.invokeAndExecute(Access.authorize, { + audience: DID.parse(provider), + with: audience, + nb: { + iss: account, + // New ucan spec moved to recap style layout for capabilities and new + // `access/request` will use similar format as opposed to legacy one, + // in the meantime we translate new format to legacy format here. + att: [...toCapabilities(access)], + }, + }) + + return result.error + ? /* c8 ignore next */ + result + : { + ok: new PendingAccessRequest({ + ...result.ok, + agent, + audience, + provider, + }), + } +} /** + * Claims access that has been delegated to the given audience, which by + * default is the agent's DID. * - * @param {{agent: API.Agent}} agent + * @param {API.Agent} agent * @param {object} input - * @param {API.Delegation[]} input.delegations - * @param {API.SpaceDID} [input.space] - * @param {API.Delegation[]} [input.proofs] + * @param {API.DID} [input.audience] - Principal requesting an access. + * @param {API.ProviderDID} [input.provider] - Provider handling the invocation. + * @returns {Promise>} + */ +export const claim = async ( + agent, + { + provider = /** @type {API.ProviderDID} */ (agent.connection.id.did()), + audience = agent.did(), + } = {} +) => { + const { out: result } = await agent.invokeAndExecute(Access.claim, { + audience: DID.parse(provider), + with: audience, + }) + + /* c8 ignore next 2 */ + if (result.error) { + return result + } else { + const delegations = Object.values(result.ok.delegations) + const proofs = delegations.flatMap((proof) => bytesToDelegations(proof)) + return { ok: new GrantedAccess({ agent, provider, audience, proofs }) } + } +} + +/** + * Represents a pending access request. It can be used to poll for the requested + * delegation. + */ +class PendingAccessRequest { + /** + * @typedef {object} PendingAccessRequestModel + * @property {API.Agent} agent - Agent handling interaction. + * @property {API.DID} audience - Principal requesting an access. + * @property {API.ProviderDID} provider - Provider handling request. + * @property {API.UTCUnixTimestamp} expiration - Seconds in UTC. + * @property {API.Link} request - Link to the `access/authorize` invocation. + * + * @param {PendingAccessRequestModel} model + */ + constructor(model) { + this.model = model + } + + get agent() { + return this.model.agent + } + get audience() { + return this.model.audience + } + get expiration() { + return new Date(this.model.expiration * 1000) + } + + get request() { + return this.model.request + } + + get provider() { + return this.model.provider + } + + /** + * Low level method and most likely you want to use `.claim` instead. This method will poll + * fetch delegations **just once** and will return proofs matching to this request. Please note + * that there may not be any matches in which case result will be `{ ok: [] }`. + * + * If you do want to continuously poll until request is approved or expired, you should use + * `.claim` method instead. + * + * @returns {Promise>} + */ + async poll() { + const { agent, audience, provider, expiration } = this.model + const timeout = expiration * 1000 - Date.now() + /* c8 ignore next 2 */ + if (timeout <= 0) { + return { error: new RequestExpired(this.model) } + } else { + const result = await claim(agent, { audience, provider }) + return result.error + ? /* c8 ignore next */ + result + : { + ok: result.ok.proofs.filter((proof) => + isRequestedAccess(proof, this.model) + ), + } + } + } + + /** + * Continuously polls delegations until this request is approved or expired. Returns + * a `GrantedAccess` object (view over the delegations) that can be used in the + * invocations or can be saved in the agent (store) using `.save()` method. + * + * @param {object} options + * @param {number} [options.interval] + * @param {AbortSignal} [options.signal] + * @returns {Promise>} + */ + async claim({ signal, interval = 250 } = {}) { + /* c8 ignore next */ + while (signal?.aborted !== true) { + const result = await this.poll() + // If polling failed, return the error. + /* c8 ignore next 3 */ + if (result.error) { + return result + } + // If we got some matching proofs, return them. + else if (result.ok.length > 0) { + return { + ok: new GrantedAccess({ + agent: this.agent, + provider: this.provider, + audience: this.audience, + proofs: result.ok, + }), + } + } + + await new Promise((resolve) => setTimeout(resolve, interval)) + } + /* c8 ignore next 4 */ + return { + error: Object.assign(new Error('Aborted'), { reason: signal.reason }), + } + } +} + +/** + * Error returned when pending access request expires. */ -export const delegate = async ({ agent }, input) => - Agent.Access.delegate(agent, input) +class RequestExpired extends Failure { + /** + * @param {PendingAccessRequestModel} model + */ + /* c8 ignore next 4 */ + constructor(model) { + super() + this.model = model + } + + /* c8 ignore next 4 */ + get name() { + return 'RequestExpired' + } + + /* c8 ignore next 3 */ + get request() { + return this.model.request + } + + /* c8 ignore next 3 */ + get expiredAt() { + return new Date(this.model.expiration * 1000) + } + + /* c8 ignore next 3 */ + describe() { + return `Access request expired at ${this.expiredAt} for ${this.request} request.` + } +} -export const { spaceAccess, accountAccess } = Agent.Access +/** + * View over the UCAN Delegations that grant access to a specific principal. + */ +class GrantedAccess { + /** + * @typedef {object} GrantedAccessModel + * @property {API.Agent} agent - Agent that processed the request. + * @property {API.DID} audience - Principal access was granted to. + * @property {API.Delegation[]} proofs - Delegations that grant access. + * @property {API.ProviderDID} provider - Provider that handled the request. + * + * @param {GrantedAccessModel} model + */ + constructor(model) { + this.model = model + } + get proofs() { + return this.model.proofs + } + get provider() { + return this.model.provider + } + get authority() { + return this.model.audience + } + + /** + * Saves access into the agents proofs store so that it can be retained + * between sessions. + * + * @param {object} input + * @param {API.Agent} [input.agent] + */ + save({ agent = this.model.agent } = {}) { + return importAuthorization(agent, this) + } +} + +/** + * Checks if the given delegation is caused by the passed `request` for access. + * + * @param {API.Delegation} delegation + * @param {object} selector + * @param {API.Link} selector.request + * @returns + */ +const isRequestedAccess = (delegation, { request }) => + // `access/confirm` handler adds facts to the delegation issued by the account + // so that principal requesting access can identify correct delegation when + // access is granted. + delegation.facts.some((fact) => `${fact['access/request']}` === `${request}`) + +/** + * Maps access object that uses UCAN 0.10 capabilities format as opposed + * to legacy UCAN 0.9 format used by w3up which predates new format. + * + * @param {API.Access} access + * @returns {{ can: API.Ability }[]} + */ +export const toCapabilities = (access) => { + const abilities = [] + const entries = /** @type {[API.Ability, API.Unit][]} */ ( + Object.entries(access) + ) + + for (const [can, details] of entries) { + if (details) { + abilities.push({ can }) + } + } + return abilities +} + +/** + * Set of capabilities required by the agent to manage a space. + */ +export const spaceAccess = { + 'space/*': {}, + 'store/*': {}, + 'upload/*': {}, + 'access/*': {}, + 'filecoin/*': {}, +} + +/** + * Set of capabilities required for by the agent to manage an account. + */ +export const accountAccess = { + '*': {}, +} diff --git a/packages/w3up-client/src/capability/provider.js b/packages/w3up-client/src/capability/provider.js new file mode 100644 index 000000000..e9c5220f3 --- /dev/null +++ b/packages/w3up-client/src/capability/provider.js @@ -0,0 +1,46 @@ +import * as API from '../agent/types.js' +import * as Provider from '@web3-storage/capabilities/provider' + +export const { Provider: ProviderDID, AccountDID } = Provider + +/** + * Provisions specified `space` with the specified `account`. It is expected + * that delegation from the account authorizing agent is either stored in the + * agent proofs or provided explicitly. + * + * @template {Record} [S=API.Service] + * @param {API.Agent} agent + * @param {object} input + * @param {API.AccountDID} input.account - Account provisioning the space. + * @param {API.SpaceDID} input.consumer - Space been provisioned. + * @param {API.ProviderDID} [input.provider] - Provider been provisioned. + * @param {API.Delegation[]} [input.proofs] - Delegation from the account + * authorizing agent to call `provider/add` capability. + */ +export const add = async ( + agent, + { + account, + consumer, + provider = /** @type {API.ProviderDID} */ (agent.connection.id.did()), + proofs, + } +) => { + /* c8 ignore next 5 */ + if (!ProviderDID.is(provider)) { + throw new Error( + `Unable to determine provider from agent.connection.id did ${provider}. expected a did:web:` + ) + } + + const { out } = await agent.invokeAndExecute(Provider.add, { + with: account, + nb: { + provider, + consumer, + }, + proofs, + }) + + return out +} diff --git a/packages/w3up-client/src/capability/space.js b/packages/w3up-client/src/capability/space.js index f2c077d28..26b91cf3d 100644 --- a/packages/w3up-client/src/capability/space.js +++ b/packages/w3up-client/src/capability/space.js @@ -1,4 +1,273 @@ import { Base } from '../base.js' +import * as ED25519 from '@ucanto/principal/ed25519' +import { delegate, Schema, UCAN } from '@ucanto/core' +import * as BIP39 from '@scure/bip39' +import { wordlist } from '@scure/bip39/wordlists/english' +import * as API from '../types.js' +import * as Access from './access.js' + +/** + * Data model for the (owned) space. + * + * @typedef {object} Model + * @property {ED25519.EdSigner} signer + * @property {string} name + */ + +/** + * Generates a new space. + * + * @param {object} options + * @param {string} options.name + */ +export const generate = async ({ name }) => { + const { signer } = await ED25519.generate() + + return new OwnedSpace({ signer, name }) +} + +/** + * Recovers space from the saved mnemonic. + * + * @param {string} mnemonic + * @param {object} options + * @param {string} options.name - Name to give to the recovered space. + */ +export const fromMnemonic = async (mnemonic, { name }) => { + const secret = BIP39.mnemonicToEntropy(mnemonic, wordlist) + const signer = await ED25519.derive(secret) + return new OwnedSpace({ signer, name }) +} + +/** + * Turns (owned) space into a BIP39 mnemonic that later can be used to recover + * the space using `fromMnemonic` function. + * + * @param {object} space + * @param {ED25519.EdSigner} space.signer + */ +export const toMnemonic = ({ signer }) => { + /** @type {Uint8Array} */ + // @ts-expect-error - Field is defined but not in the interface + const secret = signer.secret + + return BIP39.entropyToMnemonic(secret, wordlist) +} + +/** + * Creates a (UCAN) delegation that gives full access to the space to the + * specified `account`. At the moment we only allow `did:mailto` principal + * to be used as an `account`. + * + * @param {Model} space + * @param {API.AccountDID} account + */ +export const createRecovery = (space, account) => + createAuthorization(space, { + agent: space.signer.withDID(account), + access: Access.accountAccess, + expiration: Infinity, + }) + +// Default authorization session is valid for 1 year +export const SESSION_LIFETIME = 60 * 60 * 24 * 365 + +/** + * Creates (UCAN) delegation that gives specified `agent` an access to + * specified ability (passed as `access.can` field) on this space. + * Optionally, you can specify `access.expiration` field to set the + * expiration time for the authorization. By default the authorization + * is valid for 1 year and gives access to all capabilities on the space + * that are needed to use the space. + * + * @param {Model} space + * @param {object} options + * @param {API.Principal} options.agent + * @param {API.Access} [options.access] + * @param {API.UTCUnixTimestamp} [options.expiration] + */ +export const createAuthorization = async ( + { signer, name }, + { + agent, + access = Access.spaceAccess, + expiration = UCAN.now() + SESSION_LIFETIME, + } +) => { + return await delegate({ + issuer: signer, + audience: agent, + capabilities: toCapabilities({ + [signer.did()]: access, + }), + /* c8 ignore next */ + ...(expiration ? { expiration } : {}), + facts: [{ space: { name } }], + }) +} + +/** + * @param {Record} allow + * @returns {API.Capabilities} + */ +const toCapabilities = (allow) => { + const capabilities = [] + for (const [subject, access] of Object.entries(allow)) { + const entries = /** @type {[API.Ability, API.Unit][]} */ ( + Object.entries(access) + ) + + for (const [can, details] of entries) { + if (details) { + capabilities.push({ can, with: subject }) + } + } + } + + return /** @type {API.Capabilities} */ (capabilities) +} + +/** + * Represents an owned space, meaning a space for which we have a private key + * and consequently have full authority over. + */ +class OwnedSpace { + /** + * @param {Model} model + */ + constructor(model) { + this.model = model + } + + get signer() { + return this.model.signer + } + + get name() { + return this.model.name + } + + did() { + return this.signer.did() + } + + /** + * Creates a renamed version of this space. + * + * @param {string} name + */ + withName(name) { + return new OwnedSpace({ signer: this.signer, name }) + } + + /** + * Creates a (UCAN) delegation that gives full access to the space to the + * specified `account`. At the moment we only allow `did:mailto` principal + * to be used as an `account`. + * + * @param {API.AccountDID} account + */ + async createRecovery(account) { + return createRecovery(this, account) + } + + /** + * Creates (UCAN) delegation that gives specified `agent` an access to + * specified ability (passed as `access.can` field) on the this space. + * Optionally, you can specify `access.expiration` field to set the + * + * @param {API.Principal} agent + * @param {object} [input] + * @param {API.Access} [input.access] + * @param {API.UCAN.UTCUnixTimestamp} [input.expiration] + */ + createAuthorization(agent, input) { + return createAuthorization(this, { ...input, agent }) + } + + /** + * Derives BIP39 mnemonic that can be used to recover the space. + * + * @returns {string} + */ + toMnemonic() { + return toMnemonic(this) + } +} + +const SpaceDID = Schema.did({ method: 'key' }) + +/** + * Creates a (shared) space from given delegation. + * + * @param {API.Delegation} delegation + */ +export const fromDelegation = (delegation) => { + const result = SpaceDID.read(delegation.capabilities[0].with) + /* c8 ignore next 10 */ + if (result.error) { + throw Object.assign( + new Error( + `Invalid delegation, expected capabilities[0].with to be DID, ${result.error}` + ), + { + cause: result.error, + } + ) + } + + /** @type {{name?:string}} */ + /* c8 ignore next */ + const meta = delegation.facts[0]?.space ?? {} + + return new SharedSpace({ id: result.ok, delegation, meta }) +} + +/** + * Represents a shared space, meaning a space for which we have a delegation + * and consequently have limited authority over. + */ +class SharedSpace { + /** + * @typedef {object} SharedSpaceModel + * @property {API.SpaceDID} id + * @property {API.Delegation} delegation + * @property {{name?:string}} meta + * + * @param {SharedSpaceModel} model + */ + constructor(model) { + this.model = model + } + + get delegation() { + return this.model.delegation + } + + get meta() { + return this.model.meta + } + + get name() { + /* c8 ignore next */ + return this.meta.name ?? '' + } + + did() { + return this.model.id + } + + /** + * @param {string} name + */ + /* c8 ignore next 6 */ + withName(name) { + return new SharedSpace({ + ...this.model, + meta: { ...this.meta, name }, + }) + } +} /** * Client for interacting with the `space/*` capabilities. diff --git a/packages/w3up-client/src/capability/store.js b/packages/w3up-client/src/capability/store.js index 1ced89bfe..ac530578b 100644 --- a/packages/w3up-client/src/capability/store.js +++ b/packages/w3up-client/src/capability/store.js @@ -1,6 +1,12 @@ -import { Store } from '@web3-storage/upload-client' import { Store as StoreCapabilities } from '@web3-storage/capabilities' import { Base } from '../base.js' +import { CAR } from '@ucanto/transport' +import { SpaceDID } from '@web3-storage/capabilities/utils' +import retry, { AbortError } from 'p-retry' +import { servicePrincipal, connection } from './upload/service.js' +import { REQUEST_RETRIES } from './upload/constants.js' +import fetchPkg from 'ipfs-utils/src/http/fetch.js' +const { fetch } = fetchPkg /** * Client for interacting with the `store/*` capabilities. @@ -15,7 +21,7 @@ export class StoreClient extends Base { async add(car, options = {}) { const conf = await this._invocationConfig([StoreCapabilities.add.can]) options.connection = this._serviceConf.upload - return Store.add(conf, car, options) + return add(conf, car, options) } /** @@ -26,7 +32,7 @@ export class StoreClient extends Base { async list(options = {}) { const conf = await this._invocationConfig([StoreCapabilities.add.can]) options.connection = this._serviceConf.upload - return Store.list(conf, options) + return list(conf, options) } /** @@ -38,6 +44,231 @@ export class StoreClient extends Base { async remove(link, options = {}) { const conf = await this._invocationConfig([StoreCapabilities.remove.can]) options.connection = this._serviceConf.upload - return Store.remove(conf, link, options) + return remove(conf, link, options) } } + +/** + * + * @param {string} url + * @param {import('./upload/types.js').ProgressFn} handler + */ +function createUploadProgressHandler(url, handler) { + /** + * + * @param {import('./upload/types.js').ProgressStatus} status + */ + function onUploadProgress({ total, loaded, lengthComputable }) { + return handler({ total, loaded, lengthComputable, url }) + } + return onUploadProgress +} + +/** + * Store a DAG encoded as a CAR file. The issuer needs the `store/add` + * delegated capability. + * + * Required delegated capability proofs: `store/add` + * + * @param {import('./upload/types.js').InvocationConfig} conf Configuration + * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. + * + * The `issuer` is the signing authority that is issuing the UCAN + * invocation(s). It is typically the user _agent_. + * + * The `with` is the resource the invocation applies to. It is typically the + * DID of a space. + * + * The `proofs` are a set of capability delegations that prove the issuer + * has the capability to perform the action. + * + * The issuer needs the `store/add` delegated capability. + * @param {Blob|Uint8Array} car CAR file data. + * @param {import('./upload/types.js').RequestOptions} [options] + * @returns {Promise} + */ +export async function add( + { issuer, with: resource, proofs, audience }, + car, + options = {} +) { + // TODO: validate blob contains CAR data + const bytes = + car instanceof Uint8Array ? car : new Uint8Array(await car.arrayBuffer()) + const link = await CAR.codec.link(bytes) + /* c8 ignore next */ + const conn = options.connection ?? connection + const result = await retry( + async () => { + return await StoreCapabilities.add + .invoke({ + issuer, + /* c8 ignore next */ + audience: audience ?? servicePrincipal, + with: SpaceDID.from(resource), + nb: { link, size: bytes.length }, + proofs, + }) + .execute(conn) + }, + { + onFailedAttempt: console.warn, + retries: options.retries ?? REQUEST_RETRIES, + } + ) + + if (!result.out.ok) { + throw new Error(`failed ${StoreCapabilities.add.can} invocation`, { + cause: result.out.error, + }) + } + + // Return early if it was already uploaded. + if (result.out.ok.status === 'done') { + return link + } + + const responseAddUpload = result.out.ok + + const fetchWithUploadProgress = + /** @type {(url: string, init?: import('./upload/types.js').FetchOptions) => Promise} */ ( + fetch + ) + + const res = await retry( + async () => { + try { + const res = await fetchWithUploadProgress(responseAddUpload.url, { + method: 'PUT', + mode: 'cors', + body: car, + headers: responseAddUpload.headers, + signal: options.signal, + onUploadProgress: options.onUploadProgress + ? createUploadProgressHandler( + responseAddUpload.url, + options.onUploadProgress + ) + : undefined, + // @ts-expect-error - this is needed by recent versions of node - see https://github.com/bluesky-social/atproto/pull/470 for more info + duplex: 'half', + }) + if (res.status >= 400 && res.status < 500) { + throw new AbortError(`upload failed: ${res.status}`) + } + return res + } catch (err) { + if (options.signal?.aborted === true) { + throw new AbortError('upload aborted') + } + throw err + } + }, + { + retries: options.retries ?? REQUEST_RETRIES, + } + ) + + if (!res.ok) { + throw new Error(`upload failed: ${res.status}`) + } + + return link +} + +/** + * List CAR files stored by the issuer. + * + * @param {import('./upload/types.js').InvocationConfig} conf Configuration + * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. + * + * The `issuer` is the signing authority that is issuing the UCAN + * invocation(s). It is typically the user _agent_. + * + * The `with` is the resource the invocation applies to. It is typically the + * DID of a space. + * + * The `proofs` are a set of capability delegations that prove the issuer + * has the capability to perform the action. + * + * The issuer needs the `store/list` delegated capability. + * + * @param {import('./upload/types.js').ListRequestOptions} [options] + * @returns {Promise} + */ +export async function list( + { issuer, with: resource, proofs, audience }, + options = {} +) { + /* c8 ignore next */ + const conn = options.connection ?? connection + const result = await StoreCapabilities.list + .invoke({ + issuer, + /* c8 ignore next */ + audience: audience ?? servicePrincipal, + with: SpaceDID.from(resource), + proofs, + nb: { + cursor: options.cursor, + size: options.size, + pre: options.pre, + }, + }) + .execute(conn) + + if (!result.out.ok) { + throw new Error(`failed ${StoreCapabilities.list.can} invocation`, { + cause: result.out.error, + }) + } + + return result.out.ok +} + +/** + * Remove a stored CAR file by CAR CID. + * + * @param {import('./upload/types.js').InvocationConfig} conf Configuration + * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. + * + * The `issuer` is the signing authority that is issuing the UCAN + * invocation(s). It is typically the user _agent_. + * + * The `with` is the resource the invocation applies to. It is typically the + * DID of a space. + * + * The `proofs` are a set of capability delegations that prove the issuer + * has the capability to perform the action. + * + * The issuer needs the `store/remove` delegated capability. + * + * @param {import('./upload/types.js').CARLink} link CID of CAR file to remove. + * @param {import('./upload/types.js').RequestOptions} [options] + */ +export async function remove( + { issuer, with: resource, proofs, audience }, + link, + options = {} +) { + /* c8 ignore next */ + const conn = options.connection ?? connection + const result = await StoreCapabilities.remove + .invoke({ + issuer, + /* c8 ignore next */ + audience: audience ?? servicePrincipal, + with: SpaceDID.from(resource), + nb: { link }, + proofs, + }) + .execute(conn) + + if (!result.out.ok) { + throw new Error(`failed ${StoreCapabilities.remove.can} invocation`, { + cause: result.out.error, + }) + } + + return result.out +} diff --git a/packages/w3up-client/src/capability/upload.js b/packages/w3up-client/src/capability/upload.js index f27e41965..8af1c3d20 100644 --- a/packages/w3up-client/src/capability/upload.js +++ b/packages/w3up-client/src/capability/upload.js @@ -1,7 +1,12 @@ -import { Upload } from '@web3-storage/upload-client' -import { Upload as UploadCapabilities } from '@web3-storage/capabilities' +import * as UploadCapabilities from '@web3-storage/capabilities/upload' +import { SpaceDID } from '@web3-storage/capabilities/utils' +import retry from 'p-retry' +import { servicePrincipal, connection } from './upload/service.js' +import { REQUEST_RETRIES } from './upload/constants.js' import { Base } from '../base.js' +import * as API from '../types.js' +export * from './upload/index.js' /** * Client for interacting with the `upload/*` capabilities. */ @@ -16,7 +21,7 @@ export class UploadClient extends Base { async add(root, shards, options = {}) { const conf = await this._invocationConfig([UploadCapabilities.add.can]) options.connection = this._serviceConf.upload - return Upload.add(conf, root, shards, options) + return add(conf, root, shards, options) } /** @@ -27,7 +32,7 @@ export class UploadClient extends Base { async list(options = {}) { const conf = await this._invocationConfig([UploadCapabilities.list.can]) options.connection = this._serviceConf.upload - return Upload.list(conf, options) + return list(conf, options) } /** @@ -39,6 +44,163 @@ export class UploadClient extends Base { async remove(root, options = {}) { const conf = await this._invocationConfig([UploadCapabilities.remove.can]) options.connection = this._serviceConf.upload - return Upload.remove(conf, root, options) + return remove(conf, root, options) } } + +/** + * Register an "upload" with the service. The issuer needs the `upload/add` + * delegated capability. + * + * Required delegated capability proofs: `upload/add` + * + * @param {API.InvocationConfig} conf Configuration + * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. + * + * The `issuer` is the signing authority that is issuing the UCAN + * invocation(s). It is typically the user _agent_. + * + * The `with` is the resource the invocation applies to. It is typically the + * DID of a space. + * + * The `proofs` are a set of capability delegations that prove the issuer + * has the capability to perform the action. + * + * The issuer needs the `upload/add` delegated capability. + * @param {import('multiformats/link').UnknownLink} root Root data CID for the DAG that was stored. + * @param {API.CARLink[]} shards CIDs of CAR files that contain the DAG. + * @param {API.RequestOptions} [options] + * @returns {Promise} + */ +export async function add( + { issuer, with: resource, proofs, audience }, + root, + shards, + options = {} +) { + /* c8 ignore next */ + const conn = options.connection ?? connection + const result = await retry( + async () => { + return await UploadCapabilities.add + .invoke({ + issuer, + /* c8 ignore next */ + audience: audience ?? servicePrincipal, + with: SpaceDID.from(resource), + nb: { root, shards }, + proofs, + }) + .execute(conn) + }, + { + onFailedAttempt: console.warn, + retries: options.retries ?? REQUEST_RETRIES, + } + ) + + if (!result.out.ok) { + throw new Error(`failed ${UploadCapabilities.add.can} invocation`, { + cause: result.out.error, + }) + } + + return result.out.ok +} + +/** + * List uploads created by the issuer. + * + * @param {API.InvocationConfig} conf Configuration + * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. + * + * The `issuer` is the signing authority that is issuing the UCAN + * invocation(s). It is typically the user _agent_. + * + * The `with` is the resource the invocation applies to. It is typically the + * DID of a space. + * + * The `proofs` are a set of capability delegations that prove the issuer + * has the capability to perform the action. + * + * The issuer needs the `upload/list` delegated capability. + * + * @param {API.ListRequestOptions} [options] + * @returns {Promise} + */ +export async function list( + { issuer, with: resource, proofs, audience }, + options = {} +) { + /* c8 ignore next */ + const conn = options.connection ?? connection + + const result = await UploadCapabilities.list + .invoke({ + issuer, + /* c8 ignore next */ + audience: audience ?? servicePrincipal, + with: SpaceDID.from(resource), + proofs, + nb: { + cursor: options.cursor, + size: options.size, + pre: options.pre, + }, + }) + .execute(conn) + + if (!result.out.ok) { + throw new Error(`failed ${UploadCapabilities.list.can} invocation`, { + cause: result.out.error, + }) + } + + return result.out.ok +} + +/** + * Remove an upload by root data CID. + * + * @param {API.InvocationConfig} conf Configuration + * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. + * + * The `issuer` is the signing authority that is issuing the UCAN + * invocation(s). It is typically the user _agent_. + * + * The `with` is the resource the invocation applies to. It is typically the + * DID of a space. + * + * The `proofs` are a set of capability delegations that prove the issuer + * has the capability to perform the action. + * + * The issuer needs the `upload/remove` delegated capability. + * @param {API.UnknownLink} root Root data CID to remove. + * @param {API.RequestOptions} [options] + */ +export async function remove( + { issuer, with: resource, proofs, audience }, + root, + options = {} +) { + /* c8 ignore next */ + const conn = options.connection ?? connection + const result = await UploadCapabilities.remove + .invoke({ + issuer, + /* c8 ignore next */ + audience: audience ?? servicePrincipal, + with: SpaceDID.from(resource), + nb: { root }, + proofs, + }) + .execute(conn) + + if (!result.out.ok) { + throw new Error(`failed ${UploadCapabilities.remove.can} invocation`, { + cause: result.out.error, + }) + } + + return result.out.ok +} diff --git a/packages/w3up-client/src/capability/upload/car.js b/packages/w3up-client/src/capability/upload/car.js new file mode 100644 index 000000000..66253aff6 --- /dev/null +++ b/packages/w3up-client/src/capability/upload/car.js @@ -0,0 +1,115 @@ +import { CarBlockIterator, CarWriter } from '@ipld/car' +import * as dagCBOR from '@ipld/dag-cbor' +import { varint } from 'multiformats' + +/** + * @typedef {import('@ipld/unixfs').Block} Block + */ + +/** Byte length of a CBOR encoded CAR header with zero roots. */ +const NO_ROOTS_HEADER_LENGTH = 17 + +/** @param {import('./types.js').AnyLink} [root] */ +export function headerEncodingLength(root) { + if (!root) return NO_ROOTS_HEADER_LENGTH + const headerLength = dagCBOR.encode({ version: 1, roots: [root] }).length + const varintLength = varint.encodingLength(headerLength) + return varintLength + headerLength +} + +/** @param {Block} block */ +export function blockEncodingLength(block) { + const payloadLength = block.cid.bytes.length + block.bytes.length + const varintLength = varint.encodingLength(payloadLength) + return varintLength + payloadLength +} + +/** + * @param {Iterable | AsyncIterable} blocks + * @param {import('./types.js').AnyLink} [root] + * @returns {Promise} + */ +export async function encode(blocks, root) { + // @ts-expect-error + const { writer, out } = CarWriter.create(root) + /** @type {Error?} */ + let error + void (async () => { + try { + for await (const block of blocks) { + await writer.put(block) + } + } catch (/** @type {any} */ err) { + error = err + } finally { + await writer.close() + } + })() + const chunks = [] + for await (const chunk of out) chunks.push(chunk) + // @ts-expect-error + if (error != null) throw error + const roots = root != null ? [root] : [] + return Object.assign(new Blob(chunks), { version: 1, roots }) +} + +/** @extends {ReadableStream} */ +export class BlockStream extends ReadableStream { + /** @param {import('./types.js').BlobLike} car */ + constructor(car) { + /** @type {Promise?} */ + let blocksPromise = null + const getBlocksIterable = () => { + if (blocksPromise) return blocksPromise + blocksPromise = CarBlockIterator.fromIterable(toIterable(car.stream())) + return blocksPromise + } + + /** @type {AsyncIterator?} */ + let iterator = null + super({ + async start() { + const blocks = await getBlocksIterable() + iterator = /** @type {AsyncIterator} */ ( + blocks[Symbol.asyncIterator]() + ) + }, + async pull(controller) { + /* c8 ignore next */ + if (!iterator) throw new Error('missing blocks iterator') + const { value, done } = await iterator.next() + if (done) return controller.close() + controller.enqueue(value) + }, + }) + + /** @returns {Promise} */ + this.getRoots = async () => { + const blocks = await getBlocksIterable() + return await blocks.getRoots() + } + } +} + +/* c8 ignore next 20 */ +/** + * @template T + * @param {{ getReader: () => ReadableStreamDefaultReader } | AsyncIterable} stream + * @returns {AsyncIterable} + */ +function toIterable(stream) { + return Symbol.asyncIterator in stream + ? stream + : (async function* () { + const reader = stream.getReader() + try { + while (true) { + const { done, value } = await reader.read() + if (done) return + yield value + } + } finally { + reader.releaseLock() + } + })() +} diff --git a/packages/upload-client/src/constants.js b/packages/w3up-client/src/capability/upload/constants.js similarity index 100% rename from packages/upload-client/src/constants.js rename to packages/w3up-client/src/capability/upload/constants.js diff --git a/packages/w3up-client/src/capability/upload/index.js b/packages/w3up-client/src/capability/upload/index.js new file mode 100644 index 000000000..ba222c7bb --- /dev/null +++ b/packages/w3up-client/src/capability/upload/index.js @@ -0,0 +1,155 @@ +import { Parallel } from 'parallel-transform-web' +import * as PieceHasher from 'fr32-sha2-256-trunc254-padded-binary-tree-multihash/async' +import * as Link from 'multiformats/link' +import * as raw from 'multiformats/codecs/raw' +import * as Store from '../store.js' +import * as UnixFS from './unixfs.js' +import * as CAR from './car.js' +import { ShardingStream } from './sharding.js' +import { add } from '../upload.js' + +export { Store, UnixFS, CAR } +export * from './sharding.js' + +const CONCURRENT_REQUESTS = 3 + +/** + * Uploads a file to the service and returns the root data CID for the + * generated DAG. + * + * Required delegated capability proofs: `store/add`, `upload/add` + * + * @param {import('./types.js').InvocationConfig} conf Configuration + * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. + * + * The `issuer` is the signing authority that is issuing the UCAN + * invocation(s). It is typically the user _agent_. + * + * The `with` is the resource the invocation applies to. It is typically the + * DID of a space. + * + * The `proofs` are a set of capability delegations that prove the issuer + * has the capability to perform the action. + * + * The issuer needs the `store/add` and `upload/add` delegated capability. + * @param {import('./types.js').BlobLike} file File data. + * @param {import('./types.js').UploadOptions} [options] + */ +export async function uploadFile(conf, file, options = {}) { + return await uploadBlockStream( + conf, + UnixFS.createFileEncoderStream(file), + options + ) +} + +/** + * Uploads a directory of files to the service and returns the root data CID + * for the generated DAG. All files are added to a container directory, with + * paths in file names preserved. + * + * Required delegated capability proofs: `store/add`, `upload/add` + * + * @param {import('./types.js').InvocationConfig} conf Configuration + * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. + * + * The `issuer` is the signing authority that is issuing the UCAN + * invocation(s). It is typically the user _agent_. + * + * The `with` is the resource the invocation applies to. It is typically the + * DID of a space. + * + * The `proofs` are a set of capability delegations that prove the issuer + * has the capability to perform the action. + * + * The issuer needs the `store/add` and `upload/add` delegated capability. + * @param {import('./types.js').FileLike[]} files File data. + * @param {import('./types.js').UploadDirectoryOptions} [options] + */ +export async function uploadDirectory(conf, files, options = {}) { + return await uploadBlockStream( + conf, + UnixFS.createDirectoryEncoderStream(files, options), + options + ) +} + +/** + * Uploads a CAR file to the service. + * + * The difference between this function and `Store.add` is that the CAR file is + * automatically sharded and an "upload" is registered, linking the individual + * shards (see `Upload.add`). + * + * Use the `onShardStored` callback to obtain the CIDs of the CAR file shards. + * + * Required delegated capability proofs: `store/add`, `upload/add` + * + * @param {import('./types.js').InvocationConfig} conf Configuration + * for the UCAN invocation. An object with `issuer`, `with` and `proofs`. + * + * The `issuer` is the signing authority that is issuing the UCAN + * invocation(s). It is typically the user _agent_. + * + * The `with` is the resource the invocation applies to. It is typically the + * DID of a space. + * + * The `proofs` are a set of capability delegations that prove the issuer + * has the capability to perform the action. + * + * The issuer needs the `store/add` and `upload/add` delegated capability. + * @param {import('./types.js').BlobLike} car CAR file. + * @param {import('./types.js').UploadOptions} [options] + */ +export async function uploadCAR(conf, car, options = {}) { + const blocks = new CAR.BlockStream(car) + options.rootCID = options.rootCID ?? (await blocks.getRoots())[0] + return await uploadBlockStream(conf, blocks, options) +} + +/** + * @param {import('./types.js').InvocationConfig} conf + * @param {ReadableStream} blocks + * @param {import('./types.js').UploadOptions} [options] + * @returns {Promise} + */ +async function uploadBlockStream(conf, blocks, options = {}) { + /** @type {import('./types.js').CARLink[]} */ + const shards = [] + /** @type {import('./types.js').AnyLink?} */ + let root = null + const concurrency = options.concurrentRequests ?? CONCURRENT_REQUESTS + await blocks + .pipeThrough(new ShardingStream(options)) + .pipeThrough( + new Parallel(concurrency, async (car) => { + const bytes = new Uint8Array(await car.arrayBuffer()) + const [cid, piece] = await Promise.all([ + Store.add(conf, bytes, options), + (async () => { + const multihashDigest = await PieceHasher.digest(bytes) + return /** @type {import('@web3-storage/capabilities/types').PieceLink} */ ( + Link.create(raw.code, multihashDigest) + ) + })(), + ]) + const { version, roots, size } = car + return { version, roots, size, cid, piece } + }) + ) + .pipeTo( + new WritableStream({ + write(meta) { + root = root || meta.roots[0] + shards.push(meta.cid) + if (options.onShardStored) options.onShardStored(meta) + }, + }) + ) + + /* c8 ignore next */ + if (!root) throw new Error('missing root CID') + + await add(conf, root, shards, options) + return root +} diff --git a/packages/upload-client/src/service.js b/packages/w3up-client/src/capability/upload/service.js similarity index 100% rename from packages/upload-client/src/service.js rename to packages/w3up-client/src/capability/upload/service.js diff --git a/packages/w3up-client/src/capability/upload/sharding.js b/packages/w3up-client/src/capability/upload/sharding.js new file mode 100644 index 000000000..b69bdc4e0 --- /dev/null +++ b/packages/w3up-client/src/capability/upload/sharding.js @@ -0,0 +1,86 @@ +import { blockEncodingLength, encode, headerEncodingLength } from './car.js' + +// https://observablehq.com/@gozala/w3up-shard-size +const SHARD_SIZE = 133_169_152 + +/** + * Shard a set of blocks into a set of CAR files. By default the last block + * received is assumed to be the DAG root and becomes the CAR root CID for the + * last CAR output. Set the `rootCID` option to override. + * + * @extends {TransformStream} + */ +export class ShardingStream extends TransformStream { + /** + * @param {import('./types.js').ShardingOptions} [options] + */ + constructor(options = {}) { + const shardSize = options.shardSize ?? SHARD_SIZE + const maxBlockLength = shardSize - headerEncodingLength() + /** @type {import('@ipld/unixfs').Block[]} */ + let blocks = [] + /** @type {import('@ipld/unixfs').Block[] | null} */ + let readyBlocks = null + let currentLength = 0 + + super({ + async transform(block, controller) { + if (readyBlocks != null) { + controller.enqueue(await encode(readyBlocks)) + readyBlocks = null + } + + const blockLength = blockEncodingLength(block) + if (blockLength > maxBlockLength) { + throw new Error( + `block will cause CAR to exceed shard size: ${block.cid}` + ) + } + + if (blocks.length && currentLength + blockLength > maxBlockLength) { + readyBlocks = blocks + blocks = [] + currentLength = 0 + } + blocks.push(block) + currentLength += blockLength + }, + + async flush(controller) { + if (readyBlocks != null) { + controller.enqueue(await encode(readyBlocks)) + } + + const rootBlock = blocks.at(-1) + if (rootBlock == null) return + + const rootCID = options.rootCID ?? rootBlock.cid + const headerLength = headerEncodingLength(rootCID) + + // if adding CAR root overflows the shard limit we move overflowing + // blocks into a another CAR. + if (headerLength + currentLength > shardSize) { + const overage = headerLength + currentLength - shardSize + const overflowBlocks = [] + let overflowCurrentLength = 0 + while (overflowCurrentLength < overage) { + const block = blocks[blocks.length - 1] + blocks.pop() + overflowBlocks.unshift(block) + overflowCurrentLength += blockEncodingLength(block) + + // need at least 1 block in original shard + if (blocks.length < 1) + throw new Error( + `block will cause CAR to exceed shard size: ${block.cid}` + ) + } + controller.enqueue(await encode(blocks)) + controller.enqueue(await encode(overflowBlocks, rootCID)) + } else { + controller.enqueue(await encode(blocks, rootCID)) + } + }, + }) + } +} diff --git a/packages/upload-client/src/types.ts b/packages/w3up-client/src/capability/upload/types.ts similarity index 100% rename from packages/upload-client/src/types.ts rename to packages/w3up-client/src/capability/upload/types.ts diff --git a/packages/w3up-client/src/capability/upload/unixfs.js b/packages/w3up-client/src/capability/upload/unixfs.js new file mode 100644 index 000000000..3547ba71f --- /dev/null +++ b/packages/w3up-client/src/capability/upload/unixfs.js @@ -0,0 +1,176 @@ +import * as UnixFS from '@ipld/unixfs' +import * as raw from 'multiformats/codecs/raw' +import { withMaxChunkSize } from '@ipld/unixfs/file/chunker/fixed' +import { withWidth } from '@ipld/unixfs/file/layout/balanced' + +const SHARD_THRESHOLD = 1000 // shard directory after > 1,000 items +const queuingStrategy = UnixFS.withCapacity() + +const settings = UnixFS.configure({ + fileChunkEncoder: raw, + smallFileEncoder: raw, + chunker: withMaxChunkSize(1024 * 1024), + fileLayout: withWidth(1024), +}) + +/** + * @param {import('./types.js').BlobLike} blob + * @returns {Promise} + */ +export async function encodeFile(blob) { + const readable = createFileEncoderStream(blob) + const blocks = await collect(readable) + // @ts-expect-error There is always a root block + return { cid: blocks.at(-1).cid, blocks } +} + +/** + * @param {import('./types.js').BlobLike} blob + * @returns {ReadableStream} + */ +export function createFileEncoderStream(blob) { + /** @type {TransformStream} */ + const { readable, writable } = new TransformStream({}, queuingStrategy) + const unixfsWriter = UnixFS.createWriter({ writable, settings }) + const fileBuilder = new UnixFSFileBuilder('', blob) + void (async () => { + await fileBuilder.finalize(unixfsWriter) + await unixfsWriter.close() + })() + return readable +} + +class UnixFSFileBuilder { + #file + + /** + * @param {string} name + * @param {import('./types.js').BlobLike} file + */ + constructor(name, file) { + this.name = name + this.#file = file + } + + /** @param {import('@ipld/unixfs').View} writer */ + async finalize(writer) { + const unixfsFileWriter = UnixFS.createFileWriter(writer) + await this.#file.stream().pipeTo( + new WritableStream({ + async write(chunk) { + await unixfsFileWriter.write(chunk) + }, + }) + ) + return await unixfsFileWriter.close() + } +} + +class UnixFSDirectoryBuilder { + #options + + /** @type {Map} */ + entries = new Map() + + /** + * @param {string} name + * @param {import('./types.js').UnixFSDirectoryEncoderOptions} [options] + */ + constructor(name, options) { + this.name = name + this.#options = options + } + + /** @param {import('@ipld/unixfs').View} writer */ + async finalize(writer) { + const dirWriter = + this.entries.size <= SHARD_THRESHOLD + ? UnixFS.createDirectoryWriter(writer) + : UnixFS.createShardedDirectoryWriter(writer) + for (const [name, entry] of this.entries) { + const link = await entry.finalize(writer) + if (this.#options?.onDirectoryEntryLink) { + // @ts-expect-error + this.#options.onDirectoryEntryLink({ name: entry.name, ...link }) + } + dirWriter.set(name, link) + } + return await dirWriter.close() + } +} + +/** + * @param {Iterable} files + * @param {import('./types.js').UnixFSDirectoryEncoderOptions} [options] + * @returns {Promise} + */ +export async function encodeDirectory(files, options) { + const readable = createDirectoryEncoderStream(files, options) + const blocks = await collect(readable) + // @ts-expect-error There is always a root block + return { cid: blocks.at(-1).cid, blocks } +} + +/** + * @param {Iterable} files + * @param {import('./types.js').UnixFSDirectoryEncoderOptions} [options] + * @returns {ReadableStream} + */ +export function createDirectoryEncoderStream(files, options) { + const rootDir = new UnixFSDirectoryBuilder('', options) + + for (const file of files) { + const path = file.name.split('/') + if (path[0] === '' || path[0] === '.') { + path.shift() + } + let dir = rootDir + for (const [i, name] of path.entries()) { + if (i === path.length - 1) { + dir.entries.set(name, new UnixFSFileBuilder(path.join('/'), file)) + break + } + let dirBuilder = dir.entries.get(name) + if (dirBuilder == null) { + const dirName = dir === rootDir ? name : `${dir.name}/${name}` + dirBuilder = new UnixFSDirectoryBuilder(dirName, options) + dir.entries.set(name, dirBuilder) + } + if (!(dirBuilder instanceof UnixFSDirectoryBuilder)) { + throw new Error(`"${file.name}" cannot be a file and a directory`) + } + dir = dirBuilder + } + } + + /** @type {TransformStream} */ + const { readable, writable } = new TransformStream({}, queuingStrategy) + const unixfsWriter = UnixFS.createWriter({ writable, settings }) + void (async () => { + const link = await rootDir.finalize(unixfsWriter) + if (options?.onDirectoryEntryLink) { + options.onDirectoryEntryLink({ name: '', ...link }) + } + await unixfsWriter.close() + })() + + return readable +} + +/** + * @template T + * @param {ReadableStream} collectable + * @returns {Promise} + */ +async function collect(collectable) { + /** @type {T[]} */ + const chunks = [] + await collectable.pipeTo( + new WritableStream({ + write(chunk) { + chunks.push(chunk) + }, + }) + ) + return chunks +} diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 016719d76..a39715b34 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -2,7 +2,7 @@ import { uploadFile, uploadDirectory, uploadCAR, -} from '@web3-storage/upload-client' +} from './capability/upload/index.js' import { Store as StoreCapabilities, Upload as UploadCapabilities, @@ -14,13 +14,14 @@ import { StoreClient } from './capability/store.js' import { UploadClient } from './capability/upload.js' import { SpaceClient } from './capability/space.js' import { AccessClient } from './capability/access.js' +import { AgentData } from './agent.js' export * as Access from './capability/access.js' -export { StoreClient, UploadClient, SpaceClient, AccessClient } +export { StoreClient, UploadClient, SpaceClient, AccessClient, AgentData } export class Client extends Base { /** - * @param {import('@web3-storage/access').AgentData} agentData + * @param {AgentData} agentData * @param {object} [options] * @param {import('./types.js').ServiceConf} [options.serviceConf] */ diff --git a/packages/access-client/src/crypto/aes-key.js b/packages/w3up-client/src/crypto/aes-key.js similarity index 100% rename from packages/access-client/src/crypto/aes-key.js rename to packages/w3up-client/src/crypto/aes-key.js diff --git a/packages/access-client/src/crypto/encoding.js b/packages/w3up-client/src/crypto/encoding.js similarity index 100% rename from packages/access-client/src/crypto/encoding.js rename to packages/w3up-client/src/crypto/encoding.js diff --git a/packages/access-client/src/crypto/p256-ecdh.js b/packages/w3up-client/src/crypto/p256-ecdh.js similarity index 100% rename from packages/access-client/src/crypto/p256-ecdh.js rename to packages/w3up-client/src/crypto/p256-ecdh.js diff --git a/packages/access-client/src/crypto/types.ts b/packages/w3up-client/src/crypto/types.ts similarity index 100% rename from packages/access-client/src/crypto/types.ts rename to packages/w3up-client/src/crypto/types.ts diff --git a/packages/w3up-client/src/driver/conf.js b/packages/w3up-client/src/driver/conf.js new file mode 100644 index 000000000..f65fd2195 --- /dev/null +++ b/packages/w3up-client/src/driver/conf.js @@ -0,0 +1,73 @@ +import Conf from 'conf' +import * as JSON from '../agent/utils/json.js' + +/** + * @template T + * @typedef {import('./types.js').Driver} Driver + */ + +/** + * Driver implementation with "[conf](https://github.com/sindresorhus/conf)" + * + * Usage: + * + * ```js + * import { ConfDriver } from '@web3-storage/access/drivers/conf' + * ``` + * + * @template {Record} T + * @implements {Driver} + */ +export class ConfDriver { + /** + * @type {Conf} + */ + #config + + /** + * @param {{ profile: string }} opts + */ + constructor(opts) { + this.#config = new Conf({ + projectName: 'w3access', + projectSuffix: '', + configName: opts.profile, + serialize: (v) => JSON.stringify(v), + deserialize: (v) => JSON.parse(v), + }) + this.path = this.#config.path + } + + /* c8 ignore next */ + async open() {} + + /* c8 ignore next */ + async close() {} + + /* c8 ignore next 3 */ + async reset() { + this.#config.clear() + } + + /** @param {T} data */ + async save(data) { + if (typeof data === 'object') { + data = { ...data } + for (const [k, v] of Object.entries(data)) { + if (v === undefined) { + delete data[k] + } + } + } + this.#config.set(data) + } + + /** @returns {Promise} */ + async load() { + const data = + /* c8 ignore next */ + this.#config.store ?? {} + if (Object.keys(data).length === 0) return + return data + } +} diff --git a/packages/w3up-client/src/driver/indexed-db.js b/packages/w3up-client/src/driver/indexed-db.js new file mode 100644 index 000000000..4ee22a20a --- /dev/null +++ b/packages/w3up-client/src/driver/indexed-db.js @@ -0,0 +1,194 @@ +import defer from 'p-defer' + +/** + * @template T + * @typedef {import('./types.js').Driver} Driver + */ + +const STORE_NAME = 'AccessStore' +const DATA_ID = 1 + +/* c8 ignore next 150 */ +/** + * Driver implementation for the browser. + * + * Usage: + * + * ```js + * import { IndexedDBDriver } from '@web3-storage/w3up-client/driver/indexed-db' + * ``` + * + * @template T + * @implements {Driver} + */ +export class IndexedDBDriver { + /** @type {string} */ + #dbName + + /** @type {number|undefined} */ + #dbVersion + + /** @type {string} */ + #dbStoreName + + /** @type {IDBDatabase|undefined} */ + #db + + /** @type {boolean} */ + #autoOpen + + /** + * @param {string} dbName + * @param {object} [options] + * @param {number} [options.dbVersion] + * @param {string} [options.dbStoreName] + * @param {boolean} [options.autoOpen] + */ + constructor(dbName, options = {}) { + this.#dbName = dbName + this.#dbVersion = options.dbVersion + this.#dbStoreName = options.dbStoreName ?? STORE_NAME + this.#autoOpen = options.autoOpen ?? true + } + + /** @returns {Promise} */ + async #getOpenDB() { + if (!this.#db) { + if (!this.#autoOpen) throw new Error('Store is not open') + await this.open() + } + // @ts-expect-error open sets this.#db + return this.#db + } + + async open() { + const db = this.#db + if (db) return + + /** @type {import('p-defer').DeferredPromise} */ + const { resolve, reject, promise } = defer() + const openReq = indexedDB.open(this.#dbName, this.#dbVersion) + + openReq.addEventListener('upgradeneeded', () => { + const db = openReq.result + db.createObjectStore(this.#dbStoreName, { keyPath: 'id' }) + }) + + openReq.addEventListener('success', () => { + this.#db = openReq.result + resolve() + }) + + openReq.addEventListener('error', () => reject(openReq.error)) + + return promise + } + + async close() { + const db = this.#db + if (!db) throw new Error('Store is not open') + + db.close() + this.#db = undefined + } + + /** @param {T} data */ + async save(data) { + const db = await this.#getOpenDB() + + const putData = withObjectStore( + db, + 'readwrite', + this.#dbStoreName, + async (store) => { + /** @type {import('p-defer').DeferredPromise} */ + const { resolve, reject, promise } = defer() + const putReq = store.put({ id: DATA_ID, ...data }) + putReq.addEventListener('success', () => resolve()) + putReq.addEventListener('error', () => + reject(new Error('failed to query DB', { cause: putReq.error })) + ) + + return promise + } + ) + + return await putData() + } + + async load() { + const db = await this.#getOpenDB() + + const getData = withObjectStore( + db, + 'readonly', + this.#dbStoreName, + async (store) => { + /** @type {import('p-defer').DeferredPromise} */ + const { resolve, reject, promise } = defer() + + const getReq = store.get(DATA_ID) + getReq.addEventListener('success', () => resolve(getReq.result)) + getReq.addEventListener('error', () => + reject(new Error('failed to query DB', { cause: getReq.error })) + ) + + return promise + } + ) + + return await getData() + } + + async reset() { + const db = await this.#getOpenDB() + + withObjectStore(db, 'readwrite', this.#dbStoreName, (s) => { + /** @type {import('p-defer').DeferredPromise} */ + const { resolve, reject, promise } = defer() + const req = s.clear() + req.addEventListener('success', () => { + resolve() + }) + + req.addEventListener('error', () => + reject(new Error('failed to query DB', { cause: req.error })) + ) + + return promise + }) + } +} + +/* c8 ignore next 35 */ +/** + * @template T + * @param {IDBDatabase} db + * @param {IDBTransactionMode} txnMode + * @param {string} storeName + * @param {(s: IDBObjectStore) => Promise} fn + */ +function withObjectStore(db, txnMode, storeName, fn) { + return async () => { + const tx = db.transaction(storeName, txnMode) + /** @type {import('p-defer').DeferredPromise} */ + const { resolve, reject, promise } = defer() + /** @type {T} */ + let result + tx.addEventListener('complete', () => resolve(result)) + tx.addEventListener('abort', () => + reject(tx.error || new Error('transaction aborted')) + ) + tx.addEventListener('error', () => + reject(new Error('transaction error', { cause: tx.error })) + ) + try { + result = await fn(tx.objectStore(storeName)) + tx.commit() + } catch (error) { + reject(error) + tx.abort() + } + return promise + } +} diff --git a/packages/w3up-client/src/driver/memory.js b/packages/w3up-client/src/driver/memory.js new file mode 100644 index 000000000..dc4c785fb --- /dev/null +++ b/packages/w3up-client/src/driver/memory.js @@ -0,0 +1,51 @@ +/** + * @template T + * @typedef {import('./types.js').Driver} Driver + */ + +/** + * Driver implementation that stores data in memory." + * + * Usage: + * + * ```js + * import { MemoryDriver } from '@web3-storage/access/drivers/memory' + * ``` + * + * @template {Record} T + * @implements {Driver} + */ +export class MemoryDriver { + /** + * @type {T|undefined} + */ + #data + + constructor() { + this.#data = undefined + } + + /* c8 ignore next */ + async open() {} + + /* c8 ignore next */ + async close() {} + + /* c8 ignore next 3 */ + async reset() { + this.#data = undefined + } + + /** @param {T} data */ + async save(data) { + this.#data = { ...data } + } + + /** @returns {Promise} */ + async load() { + /* c8 ignore next 3 */ + if (this.#data === undefined) return + if (Object.keys(this.#data).length === 0) return + return this.#data + } +} diff --git a/packages/access-client/src/drivers/types.ts b/packages/w3up-client/src/driver/types.ts similarity index 100% rename from packages/access-client/src/drivers/types.ts rename to packages/w3up-client/src/driver/types.ts diff --git a/packages/w3up-client/src/index.js b/packages/w3up-client/src/index.js index 9b5d4f85b..10b889d69 100644 --- a/packages/w3up-client/src/index.js +++ b/packages/w3up-client/src/index.js @@ -5,10 +5,11 @@ * * @module */ -import { AgentData } from '@web3-storage/access/agent' -import { StoreIndexedDB } from '@web3-storage/access/stores/store-indexeddb' +import { AgentData } from './agent.js' +import { StoreIndexedDB } from './store/indexed-db.js' import { generate } from '@ucanto/principal/rsa' import { Client } from './client.js' +export * from './types.js' /** * Create a new w3up client. @@ -25,6 +26,7 @@ import { Client } from './client.js' * @type {import('./types.js').ClientFactory} */ export async function create(options = {}) { + /* c8 ignore next 11 */ const store = options.store ?? new StoreIndexedDB('w3up-client') const raw = await store.load() if (raw) { diff --git a/packages/w3up-client/src/index.node.js b/packages/w3up-client/src/index.node.js index 2778d4b16..caedbd8ea 100644 --- a/packages/w3up-client/src/index.node.js +++ b/packages/w3up-client/src/index.node.js @@ -2,10 +2,9 @@ * @hidden * @module */ -import { AgentData } from '@web3-storage/access/agent' -import { StoreConf } from '@web3-storage/access/stores/store-conf' import { generate } from '@ucanto/principal/ed25519' -import { Client } from './client.js' +import { Client, AgentData } from './client.js' +import { StoreConf } from './store/conf.js' /** * Create a new w3up client. diff --git a/packages/w3up-client/src/space.js b/packages/w3up-client/src/space.js index 7593ccfaa..769810f01 100644 --- a/packages/w3up-client/src/space.js +++ b/packages/w3up-client/src/space.js @@ -1,4 +1,4 @@ -export * from '@web3-storage/access/space' +export * from './capability/space.js' export class Space { /** @type {import('./types.js').DID} */ diff --git a/packages/access-client/src/stores/store-conf.js b/packages/w3up-client/src/store/conf.js similarity index 86% rename from packages/access-client/src/stores/store-conf.js rename to packages/w3up-client/src/store/conf.js index b0d91b218..7b89a7d9d 100644 --- a/packages/access-client/src/stores/store-conf.js +++ b/packages/w3up-client/src/store/conf.js @@ -1,4 +1,4 @@ -import { ConfDriver } from '../drivers/conf.js' +import { ConfDriver } from '../driver/conf.js' /** * Store implementation with "[conf](https://github.com/sindresorhus/conf)" diff --git a/packages/w3up-client/src/store/indexed-db.js b/packages/w3up-client/src/store/indexed-db.js new file mode 100644 index 000000000..eb316b816 --- /dev/null +++ b/packages/w3up-client/src/store/indexed-db.js @@ -0,0 +1,14 @@ +import { IndexedDBDriver } from '../driver/indexed-db.js' + +/** + * Store implementation for the browser. + * + * Usage: + * + * ```js + * import { StoreIndexedDB } from '@web3-storage/access/stores/store-indexeddb' + * ``` + * + * @extends {IndexedDBDriver} + */ +export class StoreIndexedDB extends IndexedDBDriver {} diff --git a/packages/access-client/src/stores/store-memory.js b/packages/w3up-client/src/store/memory.js similarity index 84% rename from packages/access-client/src/stores/store-memory.js rename to packages/w3up-client/src/store/memory.js index e9b40df55..29137fa9b 100644 --- a/packages/access-client/src/stores/store-memory.js +++ b/packages/w3up-client/src/store/memory.js @@ -1,4 +1,4 @@ -import { MemoryDriver } from '../drivers/memory.js' +import { MemoryDriver } from '../driver/memory.js' /** * Store implementation with in-memory storage diff --git a/packages/w3up-client/src/types.ts b/packages/w3up-client/src/types.ts index 5201c7109..f8f552f4c 100644 --- a/packages/w3up-client/src/types.ts +++ b/packages/w3up-client/src/types.ts @@ -1,9 +1,9 @@ -import { type Driver } from '@web3-storage/access/drivers/types' +import { type Driver } from './driver/types.js' import { type Service as AccessService, type AgentDataExport, -} from '@web3-storage/access/types' -import { type Service as UploadService } from '@web3-storage/upload-client/types' +} from './agent/types.js' +import { type Service as UploadService } from './capability/upload/types.js' import type { ConnectionView, Signer, @@ -12,16 +12,36 @@ import type { Resource, Unit, } from '@ucanto/interface' +export type { UTCUnixTimestamp } from '@ipld/dag-ucan' import { type Client } from './client.js' export * from '@ucanto/interface' export * from '@web3-storage/did-mailto' -export type { Agent, CapabilityQuery } from '@web3-storage/access/agent' +export type { + Agent, + CapabilityQuery, + BytesDelegation, + SpaceInfoResult, + EncodedDelegation, +} from './agent/types.js' + +export * from './capability/upload/types.js' +export * from './agent/types.js' +export type { DelegationOptions } from './agent/types.js' + export type { Access, AccountDID, ProviderDID, SpaceDID, -} from '@web3-storage/access/types' + UCANRevoke, + UCANRevokeSuccess, + UCANRevokeFailure, +} from './agent/types.js' + +export type { Service as UploadService } from './capability/upload/types.js' +export type { Service as AccessService } from './agent/types.js' + +export type Service = AccessService & UploadService export type ProofQuery = Record> @@ -81,7 +101,7 @@ export type { AgentDataExport, AgentMeta, DelegationMeta, -} from '@web3-storage/access/types' +} from './agent/types.js' export type { StoreAddSuccess, @@ -108,4 +128,4 @@ export type { UploadDirectoryOptions, FileLike, BlobLike, -} from '@web3-storage/upload-client/types' +} from './capability/upload/types.js' diff --git a/packages/w3up-client/test/access.test.js b/packages/w3up-client/test/access.test.js index d4f72b01e..2562f3c87 100644 --- a/packages/w3up-client/test/access.test.js +++ b/packages/w3up-client/test/access.test.js @@ -22,10 +22,12 @@ export const testAccess = { assert.deepEqual(request.audience, client.did()) assert.ok(request.expiration.getTime() >= Date.now()) + assert.ok(request.request.toString().startsWith('bafy')) const access = Result.try(await request.claim()) assert.deepEqual(access.authority, client.did()) assert.ok(access.proofs.length > 0) + assert.ok(access.provider, client.did()) const proofs = client.proofs() assert.deepEqual(proofs.length, 0) diff --git a/packages/w3up-client/test/account.test.js b/packages/w3up-client/test/account.test.js index 8170dad20..9829341ad 100644 --- a/packages/w3up-client/test/account.test.js +++ b/packages/w3up-client/test/account.test.js @@ -1,6 +1,6 @@ import * as Test from './test.js' +import * as Space from '../src/capability/space.js' import * as Account from '../src/account.js' -import * as Space from '../src/space.js' import * as Result from '../src/result.js' /** @@ -72,6 +72,8 @@ export const testAccount = { { client, mail, grantAccess } ) => { const space = await client.createSpace('test') + assert.deepEqual(space.name, 'test') + assert.deepEqual(space.withName('another').name, 'another') const mnemonic = space.toMnemonic() const { signer } = await Space.fromMnemonic(mnemonic, { name: 'import' }) assert.deepEqual( @@ -96,6 +98,9 @@ export const testAccount = { expiration: Infinity, }) + const importedSpace = Space.fromDelegation(proof) + assert.deepEqual(importedSpace.name, 'test') + await client.addSpace(proof) const info = await client.capability.space.info(space.did()) diff --git a/packages/access-client/test/agent.test.js b/packages/w3up-client/test/agent.test.js similarity index 97% rename from packages/access-client/test/agent.test.js rename to packages/w3up-client/test/agent.test.js index df10989d3..ea349abe6 100644 --- a/packages/access-client/test/agent.test.js +++ b/packages/w3up-client/test/agent.test.js @@ -1,8 +1,8 @@ import assert from 'assert' -import * as ucanto from '@ucanto/core' -import { URI } from '@ucanto/validator' +import { Schema, delegate } from '@ucanto/core' import { Delegation, provide } from '@ucanto/server' -import { Agent, Access, AgentData, connection } from '../src/agent.js' +import { Agent, AgentData, connection } from '../src/agent.js' +import * as Access from '../src/capability/access.js' import * as Space from '@web3-storage/capabilities/space' import { createServer } from './helpers/utils.js' import * as fixtures from './helpers/fixtures.js' @@ -199,12 +199,12 @@ describe('Agent', function () { async () => { await agent.invokeAndExecute(Space.info, { audience: fixtures.service, - with: URI.from(fixtures.alice.did()), + with: Schema.URI.from(fixtures.alice.did()), }) }, { name: 'Error', - message: `no proofs available for resource ${URI.from( + message: `no proofs available for resource ${Schema.URI.from( fixtures.alice.did() )} and ability space/info`, } @@ -429,7 +429,7 @@ describe('Agent', function () { const services = [serviceAWeb, serviceBWeb] for (const service of services) { // note: these delegations will have the same CID regardless of `service` - const delegation = await ucanto.delegate({ + const delegation = await delegate({ issuer: Absentee.from({ id: account }), audience: agent, capabilities: [ @@ -483,7 +483,7 @@ describe('Agent', function () { const services = [serviceAWeb, serviceBWeb] for (const service of services) { const nonce = (await ed25519.Signer.generate()).did() - const delegation = await ucanto.delegate({ + const delegation = await delegate({ issuer: Absentee.from({ id: account }), audience: agent, capabilities: [ diff --git a/packages/access-client/test/agent-data.test.js b/packages/w3up-client/test/agent/data.test.js similarity index 97% rename from packages/access-client/test/agent-data.test.js rename to packages/w3up-client/test/agent/data.test.js index 5931451c7..026c48c67 100644 --- a/packages/access-client/test/agent-data.test.js +++ b/packages/w3up-client/test/agent/data.test.js @@ -1,5 +1,5 @@ import assert from 'assert' -import { AgentData, getSessionProofs } from '../src/agent-data.js' +import { AgentData, getSessionProofs } from '../../src/agent.js' import * as ed25519 from '@ucanto/principal/ed25519' import { UCAN } from '@web3-storage/capabilities' import { Absentee } from '@ucanto/principal' diff --git a/packages/access-client/test/agent-use-cases.test.js b/packages/w3up-client/test/agent/use-cases.test.js similarity index 97% rename from packages/access-client/test/agent-use-cases.test.js rename to packages/w3up-client/test/agent/use-cases.test.js index 9783c9b51..a455ed52f 100644 --- a/packages/access-client/test/agent-use-cases.test.js +++ b/packages/w3up-client/test/agent/use-cases.test.js @@ -7,16 +7,16 @@ import * as Ucan from '@web3-storage/capabilities/ucan' import * as Space from '@web3-storage/capabilities/space' import * as Plan from '@web3-storage/capabilities/plan' import { createAuthorization } from '@web3-storage/capabilities/test/helpers/utils' -import { Agent, connection } from '../src/agent.js' +import { Agent, connection } from '../../src/agent.js' import { delegationsIncludeSessionProof, authorizeWaitAndClaim, waitForAuthorizationByPolling, getAccountPlan, -} from '../src/agent-use-cases.js' -import { createServer } from './helpers/utils.js' -import * as fixtures from './helpers/fixtures.js' -import { delegationsToBytes } from '../src/encoding.js' +} from '../../src/agent.js' +import { createServer } from '../helpers/utils.js' +import * as fixtures from '../helpers/fixtures.js' +import { delegationsToBytes } from '../../src/agent/encoding.js' describe('delegationsIncludeSessionProof', function () { it('should return true if and only if one of the delegations is a session proof', async function () { diff --git a/packages/w3up-client/test/capability/access.test.js b/packages/w3up-client/test/capability/access.test.js index 29044500b..f60b42243 100644 --- a/packages/w3up-client/test/capability/access.test.js +++ b/packages/w3up-client/test/capability/access.test.js @@ -3,9 +3,8 @@ import { create as createServer, provide } from '@ucanto/server' import * as CAR from '@ucanto/transport/car' import * as Signer from '@ucanto/principal/ed25519' import * as AccessCapabilities from '@web3-storage/capabilities/access' -import { AgentData } from '@web3-storage/access/agent' import { mockService, mockServiceConf } from '../helpers/mocks.js' -import { Client } from '../../src/client.js' +import { Client, AgentData } from '../../src/client.js' import { validateAuthorization } from '../helpers/utils.js' describe('AccessClient', () => { diff --git a/packages/w3up-client/test/capability/space.test.js b/packages/w3up-client/test/capability/space.test.js index d6519de2c..5ffeaf87a 100644 --- a/packages/w3up-client/test/capability/space.test.js +++ b/packages/w3up-client/test/capability/space.test.js @@ -3,9 +3,8 @@ import { create as createServer, provide } from '@ucanto/server' import * as CAR from '@ucanto/transport/car' import * as Signer from '@ucanto/principal/ed25519' import * as SpaceCapabilities from '@web3-storage/capabilities/space' -import { AgentData } from '@web3-storage/access/agent' import { mockService, mockServiceConf } from '../helpers/mocks.js' -import { Client } from '../../src/client.js' +import { Client, AgentData } from '../../src/client.js' import { validateAuthorization } from '../helpers/utils.js' describe('SpaceClient', () => { diff --git a/packages/w3up-client/test/capability/store.test.js b/packages/w3up-client/test/capability/store.test.js index 12f7b367a..3f82eb214 100644 --- a/packages/w3up-client/test/capability/store.test.js +++ b/packages/w3up-client/test/capability/store.test.js @@ -3,10 +3,9 @@ import { create as createServer, provide } from '@ucanto/server' import * as CAR from '@ucanto/transport/car' import * as Signer from '@ucanto/principal/ed25519' import { Store as StoreCapabilities } from '@web3-storage/capabilities' -import { AgentData } from '@web3-storage/access/agent' import { randomCAR } from '../helpers/random.js' import { mockService, mockServiceConf } from '../helpers/mocks.js' -import { Client } from '../../src/client.js' +import { Client, AgentData } from '../../src/client.js' import { validateAuthorization } from '../helpers/utils.js' describe('StoreClient', () => { diff --git a/packages/w3up-client/test/capability/upload.test.js b/packages/w3up-client/test/capability/upload.test.js index 55ca559e0..03d0ab8d7 100644 --- a/packages/w3up-client/test/capability/upload.test.js +++ b/packages/w3up-client/test/capability/upload.test.js @@ -3,10 +3,9 @@ import { create as createServer, provide } from '@ucanto/server' import * as CAR from '@ucanto/transport/car' import * as Signer from '@ucanto/principal/ed25519' import { Upload as UploadCapabilities } from '@web3-storage/capabilities' -import { AgentData } from '@web3-storage/access/agent' import { randomCAR } from '../helpers/random.js' import { mockService, mockServiceConf } from '../helpers/mocks.js' -import { Client } from '../../src/client.js' +import { Client, AgentData } from '../../src/client.js' import { validateAuthorization } from '../helpers/utils.js' describe('StoreClient', () => { diff --git a/packages/upload-client/test/car.test.js b/packages/w3up-client/test/car.test.js similarity index 94% rename from packages/upload-client/test/car.test.js rename to packages/w3up-client/test/car.test.js index c23462b02..3ed9882ae 100644 --- a/packages/upload-client/test/car.test.js +++ b/packages/w3up-client/test/car.test.js @@ -1,6 +1,6 @@ import assert from 'assert' import { CID } from 'multiformats' -import { BlockStream, encode } from '../src/car.js' +import { BlockStream, encode } from '../src/capability/upload/car.js' import { toCAR } from './helpers/car.js' import { randomBytes } from './helpers/random.js' diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index c010a6550..b57f70f97 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -5,7 +5,7 @@ import * as Signer from '@ucanto/principal/ed25519' import * as StoreCapabilities from '@web3-storage/capabilities/store' import * as UploadCapabilities from '@web3-storage/capabilities/upload' import * as UCANCapabilities from '@web3-storage/capabilities/ucan' -import { AgentData } from '@web3-storage/access/agent' +import { AgentData } from '../src/agent.js' import { randomBytes, randomCAR } from './helpers/random.js' import { toCAR } from './helpers/car.js' import { mockService, mockServiceConf } from './helpers/mocks.js' @@ -20,7 +20,7 @@ describe('Client', () => { const file = new Blob([bytes]) const expectedCar = await toCAR(bytes) - /** @type {import('@web3-storage/upload-client/types').CARLink|undefined} */ + /** @type {import('../src/index.js').CARLink|undefined} */ let carCID const service = mockService({ @@ -37,7 +37,7 @@ describe('Client', () => { status: 'upload', headers: { 'x-test': 'true' }, url: 'http://localhost:9200', - link: /** @type {import('@web3-storage/upload-client/types').CARLink} */ ( + link: /** @type {import('@web3-storage/w3up-client').CARLink} */ ( invocation.capabilities[0].nb?.link ), with: space.did(), @@ -116,7 +116,7 @@ describe('Client', () => { new File([await randomBytes(32)], '2.txt'), ] - /** @type {import('@web3-storage/upload-client/types').CARLink|undefined} */ + /** @type {import('@web3-storage/w3up-client').CARLink|undefined} */ let carCID const service = mockService({ @@ -132,7 +132,7 @@ describe('Client', () => { status: 'upload', headers: { 'x-test': 'true' }, url: 'http://localhost:9200', - link: /** @type {import('@web3-storage/upload-client/types').CARLink} */ ( + link: /** @type {import('@web3-storage/w3up-client').CARLink} */ ( invocation.capabilities[0].nb?.link ), with: space.did(), @@ -210,7 +210,7 @@ describe('Client', () => { status: 'upload', headers: { 'x-test': 'true' }, url: 'http://localhost:9200', - link: /** @type {import('@web3-storage/upload-client/types').CARLink} */ ( + link: /** @type {import('@web3-storage/w3up-client').CARLink} */ ( invocation.capabilities[0].nb?.link ), with: space.did(), diff --git a/packages/access-client/test/drivers/conf.node.test.js b/packages/w3up-client/test/drivers/conf.node.test.js similarity index 93% rename from packages/access-client/test/drivers/conf.node.test.js rename to packages/w3up-client/test/drivers/conf.node.test.js index fc4166a48..60c271ea6 100644 --- a/packages/access-client/test/drivers/conf.node.test.js +++ b/packages/w3up-client/test/drivers/conf.node.test.js @@ -1,6 +1,6 @@ import assert from 'assert' import { Buffer } from 'node:buffer' -import { ConfDriver } from '../../src/drivers/conf.js' +import { ConfDriver } from '../../src/driver/conf.js' describe('Conf driver', () => { it('should not fail on to store undefined value', async () => { diff --git a/packages/access-client/test/encoding.test.js b/packages/w3up-client/test/encoding.test.js similarity index 99% rename from packages/access-client/test/encoding.test.js rename to packages/w3up-client/test/encoding.test.js index 7d7d48291..b961da8d6 100644 --- a/packages/access-client/test/encoding.test.js +++ b/packages/w3up-client/test/encoding.test.js @@ -7,7 +7,7 @@ import { delegationsToBytes, delegationsToString, stringToDelegations, -} from '../src/encoding.js' +} from '../src/agent/encoding.js' describe('Encoding', function () { it('delegationsToBytes should fail with empty array', async function () { diff --git a/packages/upload-client/test/helpers/block.js b/packages/w3up-client/test/helpers/block.js similarity index 100% rename from packages/upload-client/test/helpers/block.js rename to packages/w3up-client/test/helpers/block.js diff --git a/packages/access-client/test/helpers/fixtures.js b/packages/w3up-client/test/helpers/fixtures.js similarity index 92% rename from packages/access-client/test/helpers/fixtures.js rename to packages/w3up-client/test/helpers/fixtures.js index 5f03c4314..de9b2d243 100644 --- a/packages/access-client/test/helpers/fixtures.js +++ b/packages/w3up-client/test/helpers/fixtures.js @@ -13,6 +13,7 @@ export const mallory = Signer.parse( 'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=' ) +/** did:key:z6MkrZ1r5XBFZjBU34qyD8fueMbMRkKw17BZaq2ivKFjnz2z */ export const service = Signer.parse( 'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=' ) diff --git a/packages/w3up-client/test/helpers/mocks.js b/packages/w3up-client/test/helpers/mocks.js index 7f98a3544..609dfbf24 100644 --- a/packages/w3up-client/test/helpers/mocks.js +++ b/packages/w3up-client/test/helpers/mocks.js @@ -8,23 +8,25 @@ const notImplemented = () => { /** * @param {Partial<{ - * access: Partial - * provider: Partial - * store: Partial - * upload: Partial - * space: Partial - * ucan: Partial + * access: Partial + * provider: Partial + * store: Partial + * upload: Partial + * space: Partial + * ucan: Partial * }>} impl */ export function mockService(impl) { return { store: { add: withCallCount(impl.store?.add ?? notImplemented), + get: withCallCount(impl.store?.get ?? notImplemented), list: withCallCount(impl.store?.list ?? notImplemented), remove: withCallCount(impl.store?.remove ?? notImplemented), }, upload: { add: withCallCount(impl.upload?.add ?? notImplemented), + get: withCallCount(impl.upload?.get ?? notImplemented), list: withCallCount(impl.upload?.list ?? notImplemented), remove: withCallCount(impl.upload?.remove ?? notImplemented), }, diff --git a/packages/w3up-client/test/helpers/random.js b/packages/w3up-client/test/helpers/random.js index b7e8aedb4..b2aafd1ce 100644 --- a/packages/w3up-client/test/helpers/random.js +++ b/packages/w3up-client/test/helpers/random.js @@ -1,4 +1,5 @@ import { toCAR } from './car.js' +import { toBlock } from './block.js' /** @param {number} size */ export async function randomBytes(size) { @@ -29,3 +30,9 @@ export async function randomCAR(size) { const bytes = await randomBytes(size) return toCAR(bytes) } + +/** @param {number} size */ +export async function randomBlock(size) { + const bytes = await randomBytes(size) + return await toBlock(bytes) +} diff --git a/packages/w3up-client/test/helpers/utils.js b/packages/w3up-client/test/helpers/utils.js index c173e7e70..972441c10 100644 --- a/packages/w3up-client/test/helpers/utils.js +++ b/packages/w3up-client/test/helpers/utils.js @@ -1 +1,66 @@ +// eslint-disable-next-line no-unused-vars +import * as Ucanto from '@ucanto/interface' +import { parseLink } from '@ucanto/core' +import * as Server from '@ucanto/server' +import * as Space from '@web3-storage/capabilities/space' +import * as CAR from '@ucanto/transport/car' +import * as CBOR from '@ucanto/core/cbor' +import { service } from './fixtures.js' + +/** + * @param {string} source + */ +export function parseCarLink(source) { + return /** @type {Ucanto.Link} */ (parseLink(source)) +} + +/** + * @param {any} data + */ +export async function createCborCid(data) { + const cbor = await CBOR.write(data) + return cbor.cid +} + +/** + * @param {string} source + */ +export async function createCarCid(source) { + const cbor = await CBOR.write({ hello: source }) + const shard = await CAR.codec.write({ roots: [cbor] }) + return shard.cid +} + +/** + * @param {object} handlers - a map of keys to capability handler maps + * @returns {Ucanto.ServerView} + */ +export function createServer(handlers = {}) { + const server = Server.create({ + id: service, + codec: CAR.inbound, + service: { + space: { + info: Server.provide(Space.info, async ({ capability }) => { + return { + ok: { + did: 'did:key:sss', + agent: 'did:key:agent', + email: 'mail@mail.com', + product: 'product:free', + updated_at: 'sss', + inserted_at: 'date', + }, + } + }), + }, + ...handlers, + }, + validateAuthorization, + }) + + // @ts-ignore + return server +} + export const validateAuthorization = () => ({ ok: {} }) diff --git a/packages/w3up-client/test/index.node.test.js b/packages/w3up-client/test/index.node.test.js index f9f263c2b..6b801dfcf 100644 --- a/packages/w3up-client/test/index.node.test.js +++ b/packages/w3up-client/test/index.node.test.js @@ -1,7 +1,7 @@ import assert from 'assert' import { Signer } from '@ucanto/principal/ed25519' import { EdDSA } from '@ipld/dag-ucan/signature' -import { StoreConf } from '@web3-storage/access/stores/store-conf' +import { StoreConf } from '../src/store/conf.js' import { create } from '../src/index.node.js' describe('create', () => { diff --git a/packages/upload-client/test/sharding.test.js b/packages/w3up-client/test/sharding.test.js similarity index 96% rename from packages/upload-client/test/sharding.test.js rename to packages/w3up-client/test/sharding.test.js index a40f22f9e..d8b7a31f7 100644 --- a/packages/upload-client/test/sharding.test.js +++ b/packages/w3up-client/test/sharding.test.js @@ -1,7 +1,7 @@ import assert from 'assert' import { CID } from 'multiformats' -import { createFileEncoderStream } from '../src/unixfs.js' -import { ShardingStream } from '../src/sharding.js' +import { createFileEncoderStream } from '../src/capability/upload/unixfs.js' +import { ShardingStream } from '../src/capability/upload/sharding.js' import { randomBlock, randomBytes } from './helpers/random.js' describe('ShardingStream', () => { diff --git a/packages/upload-client/test/store.test.js b/packages/w3up-client/test/store.test.js similarity index 99% rename from packages/upload-client/test/store.test.js rename to packages/w3up-client/test/store.test.js index c6e81f84b..65040bc13 100644 --- a/packages/upload-client/test/store.test.js +++ b/packages/w3up-client/test/store.test.js @@ -5,8 +5,8 @@ import { provide } from '@ucanto/server' import * as CAR from '@ucanto/transport/car' import * as Signer from '@ucanto/principal/ed25519' import * as StoreCapabilities from '@web3-storage/capabilities/store' -import * as Store from '../src/store.js' -import { serviceSigner } from './fixtures.js' +import * as Store from '../src/capability/store.js' +import { service as serviceSigner } from './helpers/fixtures.js' import { randomCAR } from './helpers/random.js' import { mockService } from './helpers/mocks.js' import { validateAuthorization } from './helpers/utils.js' diff --git a/packages/access-client/test/stores/store-indexeddb.browser.test.js b/packages/w3up-client/test/stores/store-indexeddb.browser.test.js similarity index 96% rename from packages/access-client/test/stores/store-indexeddb.browser.test.js rename to packages/w3up-client/test/stores/store-indexeddb.browser.test.js index 81495e132..d7dd0111f 100644 --- a/packages/access-client/test/stores/store-indexeddb.browser.test.js +++ b/packages/w3up-client/test/stores/store-indexeddb.browser.test.js @@ -2,8 +2,8 @@ import assert from 'assert' import { top } from '@web3-storage/capabilities/top' import { Signer as EdSigner } from '@ucanto/principal/ed25519' import * as RSASigner from '@ucanto/principal/rsa' -import { AgentData } from '../../src/agent-data.js' -import { StoreIndexedDB } from '../../src/stores/store-indexeddb.js' +import { AgentData } from '../../src/agent.js' +import { StoreIndexedDB } from '../../src/store/indexed-db.js' describe('IndexedDB store', () => { it('should create and load data', async () => { diff --git a/packages/w3up-client/test/test.js b/packages/w3up-client/test/test.js index 5ffe569ae..84d001b37 100644 --- a/packages/w3up-client/test/test.js +++ b/packages/w3up-client/test/test.js @@ -1,6 +1,7 @@ -import { StoreMemory } from '@web3-storage/access/stores/store-memory' +import { StoreMemory } from '../src/store/memory.js' +import * as Client from '../src/index.js' +// @ts-ignore - break the circular dependency between @web3-storage/upload-api and @web3-storage/w3up-client import * as Context from '@web3-storage/upload-api/test/context' -import * as Client from '@web3-storage/w3up-client' import * as assert from 'assert' /** diff --git a/packages/upload-client/test/unixfs.test.js b/packages/w3up-client/test/unixfs.test.js similarity index 97% rename from packages/upload-client/test/unixfs.test.js rename to packages/w3up-client/test/unixfs.test.js index 2a45be0ad..ba47b0b77 100644 --- a/packages/upload-client/test/unixfs.test.js +++ b/packages/w3up-client/test/unixfs.test.js @@ -5,7 +5,7 @@ import { exporter } from 'ipfs-unixfs-exporter' import { MemoryBlockstore } from 'blockstore-core/memory' import * as raw from 'multiformats/codecs/raw' import path from 'path' -import { encodeFile, encodeDirectory } from '../src/unixfs.js' +import { encodeFile, encodeDirectory } from '../src/capability/upload/unixfs.js' import { File } from './helpers/shims.js' /** @param {import('ipfs-unixfs-exporter').UnixFSDirectory} dir */ @@ -110,7 +110,7 @@ describe('UnixFS', () => { new File(['file'], 'file.txt'), new File(['another'], '/dir/another.txt'), ] - /** @type {import('../src/types.js').DirectoryEntryLink[]} */ + /** @type {import('../src/index.js').DirectoryEntryLink[]} */ const links = [] await encodeDirectory(files, { onDirectoryEntryLink: (l) => links.push(l) }) assert.equal(links.length, 4) diff --git a/packages/upload-client/test/upload.test.js b/packages/w3up-client/test/upload.test.js similarity index 99% rename from packages/upload-client/test/upload.test.js rename to packages/w3up-client/test/upload.test.js index a22b10848..ac49c188d 100644 --- a/packages/upload-client/test/upload.test.js +++ b/packages/w3up-client/test/upload.test.js @@ -5,8 +5,8 @@ import { provide } from '@ucanto/server' import * as CAR from '@ucanto/transport/car' import * as Signer from '@ucanto/principal/ed25519' import * as UploadCapabilities from '@web3-storage/capabilities/upload' -import * as Upload from '../src/upload.js' -import { serviceSigner } from './fixtures.js' +import * as Upload from '../src/capability/upload.js' +import { service as serviceSigner } from './helpers/fixtures.js' import { randomCAR } from './helpers/random.js' import { mockService } from './helpers/mocks.js' import { validateAuthorization } from './helpers/utils.js' diff --git a/packages/upload-client/test/index.test.js b/packages/w3up-client/test/uploader.test.js similarity index 98% rename from packages/upload-client/test/index.test.js rename to packages/w3up-client/test/uploader.test.js index a5ffb6055..2c46755d7 100644 --- a/packages/upload-client/test/index.test.js +++ b/packages/w3up-client/test/uploader.test.js @@ -6,8 +6,12 @@ import * as CAR from '@ucanto/transport/car' import * as Signer from '@ucanto/principal/ed25519' import * as StoreCapabilities from '@web3-storage/capabilities/store' import * as UploadCapabilities from '@web3-storage/capabilities/upload' -import { uploadFile, uploadDirectory, uploadCAR } from '../src/index.js' -import { serviceSigner } from './fixtures.js' +import { + uploadFile, + uploadDirectory, + uploadCAR, +} from '../src/capability/upload.js' +import { service as serviceSigner } from './helpers/fixtures.js' import { randomBlock, randomBytes } from './helpers/random.js' import { toCAR } from './helpers/car.js' import { File } from './helpers/shims.js' @@ -17,7 +21,7 @@ import { blockEncodingLength, encode, headerEncodingLength, -} from '../src/car.js' +} from '../src/capability/upload/car.js' import { toBlock } from './helpers/block.js' describe('uploadFile', () => { diff --git a/packages/w3up-client/tsconfig.json b/packages/w3up-client/tsconfig.json index 1c359f7c4..f5242ea4f 100644 --- a/packages/w3up-client/tsconfig.json +++ b/packages/w3up-client/tsconfig.json @@ -16,11 +16,5 @@ }, "customCss": "./static/docs.css" }, - "references": [ - { "path": "../access-client" }, - { "path": "../capabilities" }, - { "path": "../upload-client" }, - { "path": "../did-mailto" }, - { "path": "../upload-api" } - ] + "references": [{ "path": "../capabilities" }, { "path": "../did-mailto" }] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 070946331..6ec4cf4d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,7 +26,7 @@ importers: version: 2.4.3(typescript@5.2.2) docusaurus-plugin-typedoc: specifier: ^0.18.0 - version: 0.18.0(typedoc-plugin-markdown@3.17.0)(typedoc@0.25.3) + version: 0.18.0(typedoc-plugin-markdown@3.17.1)(typedoc@0.25.3) lint-staged: specifier: ^13.2.0 version: 13.3.0 @@ -35,113 +35,23 @@ importers: version: 2.8.3 typedoc-plugin-markdown: specifier: ^3.14.0 - version: 3.17.0(typedoc@0.25.3) + version: 3.17.1(typedoc@0.25.3) typescript: specifier: 5.2.2 version: 5.2.2 packages/access-client: dependencies: - '@ipld/car': - specifier: ^5.1.1 - version: 5.2.4 - '@ipld/dag-ucan': - specifier: ^3.4.0 - version: 3.4.0 - '@scure/bip39': - specifier: ^1.2.1 - version: 1.2.1 - '@ucanto/client': - specifier: ^9.0.0 - version: 9.0.0 - '@ucanto/core': - specifier: ^9.0.0 - version: 9.0.0 - '@ucanto/interface': - specifier: ^9.0.0 - version: 9.0.0 - '@ucanto/principal': - specifier: ^9.0.0 - version: 9.0.0 - '@ucanto/transport': - specifier: ^9.0.0 - version: 9.0.0 - '@ucanto/validator': - specifier: ^9.0.0 - version: 9.0.0 - '@web3-storage/capabilities': - specifier: workspace:^ - version: link:../capabilities - '@web3-storage/did-mailto': + '@web3-storage/w3up-client': specifier: workspace:^ - version: link:../did-mailto - bigint-mod-arith: - specifier: ^3.1.2 - version: 3.3.1 - conf: - specifier: 11.0.2 - version: 11.0.2 - multiformats: - specifier: ^12.1.2 - version: 12.1.3 - one-webcrypto: - specifier: git://github.com/web3-storage/one-webcrypto - version: github.com/web3-storage/one-webcrypto/5148cd14d5489a8ac4cd38223870e02db15a2382 - p-defer: - specifier: ^4.0.0 - version: 4.0.0 - type-fest: - specifier: ^3.3.0 - version: 3.13.1 - uint8arrays: - specifier: ^4.0.6 - version: 4.0.6 + version: link:../w3up-client devDependencies: - '@types/assert': - specifier: ^1.5.6 - version: 1.5.8 - '@types/inquirer': - specifier: ^9.0.4 - version: 9.0.6 - '@types/mocha': - specifier: ^10.0.1 - version: 10.0.3 - '@types/node': - specifier: ^20.8.4 - version: 20.8.10 - '@types/sinon': - specifier: ^10.0.19 - version: 10.0.20 - '@types/varint': - specifier: ^6.0.1 - version: 6.0.2 - '@types/ws': - specifier: ^8.5.4 - version: 8.5.8 - '@ucanto/server': - specifier: ^9.0.1 - version: 9.0.1 '@web3-storage/eslint-config-w3up': specifier: workspace:^ version: link:../eslint-config-w3up - assert: - specifier: ^2.0.0 - version: 2.1.0 - mocha: - specifier: ^10.2.0 - version: 10.2.0 - playwright-test: - specifier: ^12.3.4 - version: 12.4.3 - sinon: - specifier: ^15.0.3 - version: 15.2.0 typescript: specifier: 5.2.2 version: 5.2.2 - watch: - specifier: ^1.0.2 - version: 1.0.2 packages/capabilities: dependencies: @@ -166,7 +76,7 @@ importers: devDependencies: '@types/assert': specifier: ^1.5.6 - version: 1.5.8 + version: 1.5.9 '@types/mocha': specifier: ^10.0.0 version: 10.0.3 @@ -199,7 +109,7 @@ importers: devDependencies: '@types/assert': specifier: ^1.5.6 - version: 1.5.8 + version: 1.5.9 '@types/mocha': specifier: ^10.0.1 version: 10.0.3 @@ -214,16 +124,16 @@ importers: dependencies: '@typescript-eslint/eslint-plugin': specifier: ^6.9.1 - version: 6.9.1(@typescript-eslint/parser@6.9.1)(eslint@8.52.0)(typescript@5.2.2) + version: 6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2) '@typescript-eslint/parser': specifier: ^6.9.1 - version: 6.9.1(eslint@8.52.0)(typescript@5.2.2) + version: 6.10.0(eslint@8.53.0)(typescript@5.2.2) eslint: specifier: '>= 8' - version: 8.52.0 + version: 8.53.0 eslint-plugin-jsdoc: specifier: ^46.8.2 - version: 46.8.2(eslint@8.52.0) + version: 46.8.2(eslint@8.53.0) packages/filecoin-api: dependencies: @@ -312,7 +222,7 @@ importers: version: 10.1.5 '@types/assert': specifier: ^1.5.6 - version: 1.5.8 + version: 1.5.9 '@types/mocha': specifier: ^10.0.1 version: 10.0.3 @@ -373,9 +283,6 @@ importers: '@ucanto/validator': specifier: ^9.0.0 version: 9.0.0 - '@web3-storage/access': - specifier: workspace:^ - version: link:../access-client '@web3-storage/capabilities': specifier: workspace:^ version: link:../capabilities @@ -385,6 +292,9 @@ importers: '@web3-storage/filecoin-api': specifier: workspace:^ version: link:../filecoin-api + '@web3-storage/w3up-client': + specifier: workspace:^ + version: link:../w3up-client multiformats: specifier: ^12.1.2 version: 12.1.3 @@ -424,6 +334,19 @@ importers: version: github.com/web3-storage/one-webcrypto/5148cd14d5489a8ac4cd38223870e02db15a2382 packages/upload-client: + dependencies: + '@web3-storage/w3up-client': + specifier: workspace:^ + version: link:../w3up-client + devDependencies: + '@web3-storage/eslint-config-w3up': + specifier: workspace:^ + version: link:../eslint-config-w3up + typescript: + specifier: 5.2.2 + version: 5.2.2 + + packages/w3up-client: dependencies: '@ipld/car': specifier: ^5.2.2 @@ -437,18 +360,36 @@ importers: '@ipld/unixfs': specifier: ^2.1.1 version: 2.1.2 + '@scure/bip39': + specifier: ^1.2.1 + version: 1.2.1 '@ucanto/client': specifier: ^9.0.0 version: 9.0.0 + '@ucanto/core': + specifier: ^9.0.0 + version: 9.0.0 '@ucanto/interface': specifier: ^9.0.0 version: 9.0.0 + '@ucanto/principal': + specifier: ^9.0.0 + version: 9.0.0 '@ucanto/transport': specifier: ^9.0.0 version: 9.0.0 '@web3-storage/capabilities': specifier: workspace:^ version: link:../capabilities + '@web3-storage/did-mailto': + specifier: workspace:^ + version: link:../did-mailto + bigint-mod-arith: + specifier: ^3.1.2 + version: 3.3.1 + conf: + specifier: 11.0.2 + version: 11.0.2 fr32-sha2-256-trunc254-padded-binary-tree-multihash: specifier: ^3.1.0 version: 3.1.0 @@ -458,110 +399,37 @@ importers: multiformats: specifier: ^12.1.2 version: 12.1.3 + one-webcrypto: + specifier: git://github.com/web3-storage/one-webcrypto + version: github.com/web3-storage/one-webcrypto/5148cd14d5489a8ac4cd38223870e02db15a2382 + p-defer: + specifier: ^4.0.0 + version: 4.0.0 p-retry: specifier: ^5.1.2 version: 5.1.2 parallel-transform-web: specifier: ^1.0.0 version: 1.0.0 - varint: - specifier: ^6.0.0 - version: 6.0.0 - devDependencies: - '@types/assert': - specifier: ^1.5.6 - version: 1.5.8 - '@types/mocha': - specifier: ^10.0.1 - version: 10.0.3 - '@types/varint': - specifier: ^6.0.1 - version: 6.0.2 - '@ucanto/principal': - specifier: ^9.0.0 - version: 9.0.0 - '@ucanto/server': - specifier: ^9.0.1 - version: 9.0.1 - '@web3-storage/eslint-config-w3up': - specifier: workspace:^ - version: link:../eslint-config-w3up - assert: - specifier: ^2.0.0 - version: 2.1.0 - blockstore-core: - specifier: ^3.0.0 - version: 3.0.0 - c8: - specifier: ^7.13.0 - version: 7.14.0 - hundreds: - specifier: ^0.0.9 - version: 0.0.9 - ipfs-unixfs-exporter: - specifier: ^10.0.0 - version: 10.0.1 - mocha: - specifier: ^10.2.0 - version: 10.2.0 - npm-run-all: - specifier: ^4.1.5 - version: 4.1.5 - playwright-test: - specifier: ^12.3.4 - version: 12.4.3 - typescript: - specifier: 5.2.2 - version: 5.2.2 - - packages/w3up-client: - dependencies: - '@ipld/dag-ucan': - specifier: ^3.4.0 - version: 3.4.0 - '@ucanto/client': - specifier: ^9.0.0 - version: 9.0.0 - '@ucanto/core': - specifier: ^9.0.0 - version: 9.0.0 - '@ucanto/interface': - specifier: ^9.0.0 - version: 9.0.0 - '@ucanto/principal': - specifier: ^9.0.0 - version: 9.0.0 - '@ucanto/transport': - specifier: ^9.0.0 - version: 9.0.0 - '@web3-storage/access': - specifier: workspace:^ - version: link:../access-client - '@web3-storage/capabilities': - specifier: workspace:^ - version: link:../capabilities - '@web3-storage/did-mailto': - specifier: workspace:^ - version: link:../did-mailto - '@web3-storage/upload-client': - specifier: workspace:^ - version: link:../upload-client + uint8arrays: + specifier: ^4.0.6 + version: 4.0.6 devDependencies: '@docusaurus/core': specifier: ^2.2.0 version: 2.4.3(typescript@5.2.2) - '@ipld/car': - specifier: ^5.1.1 - version: 5.2.4 '@types/assert': specifier: ^1.5.6 - version: 1.5.8 + version: 1.5.9 '@types/mocha': specifier: ^10.0.1 version: 10.0.3 '@types/node': specifier: ^20.8.4 version: 20.8.10 + '@types/sinon': + specifier: ^10.0.19 + version: 10.0.20 '@ucanto/server': specifier: ^9.0.1 version: 9.0.1 @@ -574,33 +442,39 @@ importers: assert: specifier: ^2.0.0 version: 2.1.0 + blockstore-core: + specifier: ^3.0.0 + version: 3.0.0 c8: specifier: ^7.13.0 version: 7.14.0 docusaurus-plugin-typedoc: specifier: ^0.18.0 - version: 0.18.0(typedoc-plugin-markdown@3.17.0)(typedoc@0.23.28) + version: 0.18.0(typedoc-plugin-markdown@3.17.1)(typedoc@0.23.28) hundreds: specifier: ^0.0.9 version: 0.0.9 + ipfs-unixfs-exporter: + specifier: ^10.0.0 + version: 10.0.1 mocha: specifier: ^10.1.0 version: 10.2.0 - multiformats: - specifier: ^12.1.2 - version: 12.1.3 npm-run-all: specifier: ^4.1.5 version: 4.1.5 playwright-test: specifier: ^12.3.4 version: 12.4.3 + sinon: + specifier: ^15.0.3 + version: 15.2.0 typedoc: specifier: ^0.23.24 version: 0.23.28(typescript@5.2.2) typedoc-plugin-markdown: specifier: ^3.14.0 - version: 3.17.0(typedoc@0.23.28) + version: 3.17.1(typedoc@0.23.28) typedoc-plugin-missing-exports: specifier: ^1.0.0 version: 1.0.0(typedoc@0.23.28) @@ -2208,7 +2082,7 @@ packages: react: optional: true dependencies: - '@types/react': 18.2.34 + '@types/react': 18.2.36 prop-types: 15.8.1 dev: true @@ -2482,13 +2356,13 @@ packages: dev: true optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.52.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.52.0 + eslint: 8.53.0 eslint-visitor-keys: 3.4.3 dev: false @@ -2497,8 +2371,8 @@ packages: engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: false - /@eslint/eslintrc@2.1.2: - resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} + /@eslint/eslintrc@2.1.3: + resolution: {integrity: sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 @@ -2514,8 +2388,8 @@ packages: - supports-color dev: false - /@eslint/js@8.52.0: - resolution: {integrity: sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==} + /@eslint/js@8.53.0: + resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: false @@ -2615,10 +2489,10 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.5 - '@types/istanbul-reports': 3.0.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 '@types/node': 20.8.10 - '@types/yargs': 17.0.29 + '@types/yargs': 17.0.30 chalk: 4.1.2 dev: true @@ -3011,56 +2885,56 @@ packages: engines: {node: '>=10.13.0'} dev: true - /@types/assert@1.5.8: - resolution: {integrity: sha512-tL1NSDf4CF5hiVTnLd4KSth6bmRO3+tw8cJzEAUaN6fYQ26DIixX0lRFmtrA0jhxUS8SL6PDWtphMz3maxapjA==} + /@types/assert@1.5.9: + resolution: {integrity: sha512-uQ+/hp0DbfrhnJFgiqHlsrLTGgr8HZGssV7vYN8XunlvyJkzArcyBy/9e5Rey2T7lpL7jVuoNI/8CWCDCwISgg==} dev: true - /@types/body-parser@1.19.4: - resolution: {integrity: sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA==} + /@types/body-parser@1.19.5: + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: - '@types/connect': 3.4.37 + '@types/connect': 3.4.38 '@types/node': 20.8.10 dev: true - /@types/bonjour@3.5.12: - resolution: {integrity: sha512-ky0kWSqXVxSqgqJvPIkgFkcn4C8MnRog308Ou8xBBIVo39OmUFy+jqNe0nPwLCDFxUpmT9EvT91YzOJgkDRcFg==} + /@types/bonjour@3.5.13: + resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} dependencies: '@types/node': 20.8.10 dev: true - /@types/connect-history-api-fallback@1.5.2: - resolution: {integrity: sha512-gX2j9x+NzSh4zOhnRPSdPPmTepS4DfxES0AvIFv3jGv5QyeAJf6u6dY5/BAoAJU9Qq1uTvwOku8SSC2GnCRl6Q==} + /@types/connect-history-api-fallback@1.5.3: + resolution: {integrity: sha512-6mfQ6iNvhSKCZJoY6sIG3m0pKkdUcweVNOLuBBKvoWGzl2yRxOJcYOTRyLKt3nxXvBLJWa6QkW//tgbIwJehmA==} dependencies: - '@types/express-serve-static-core': 4.17.39 + '@types/express-serve-static-core': 4.17.41 '@types/node': 20.8.10 dev: true - /@types/connect@3.4.37: - resolution: {integrity: sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==} + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: '@types/node': 20.8.10 dev: true - /@types/eslint-scope@3.7.6: - resolution: {integrity: sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==} + /@types/eslint-scope@3.7.7: + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} dependencies: - '@types/eslint': 8.44.6 - '@types/estree': 1.0.4 + '@types/eslint': 8.44.7 + '@types/estree': 1.0.5 dev: true - /@types/eslint@8.44.6: - resolution: {integrity: sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==} + /@types/eslint@8.44.7: + resolution: {integrity: sha512-f5ORu2hcBbKei97U73mf+l9t4zTGl74IqZ0GQk4oVea/VS8tQZYkUveSYojk+frraAVYId0V2WC9O4PTNru2FQ==} dependencies: - '@types/estree': 1.0.4 - '@types/json-schema': 7.0.14 + '@types/estree': 1.0.5 + '@types/json-schema': 7.0.15 dev: true - /@types/estree@1.0.4: - resolution: {integrity: sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==} + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true - /@types/express-serve-static-core@4.17.39: - resolution: {integrity: sha512-BiEUfAiGCOllomsRAZOiMFP7LAnrifHpt56pc4Z7l9K6ACyN06Ns1JLMBxwkfLOjJRlSf06NwWsT7yzfpaVpyQ==} + /@types/express-serve-static-core@4.17.41: + resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==} dependencies: '@types/node': 20.8.10 '@types/qs': 6.9.9 @@ -3068,11 +2942,11 @@ packages: '@types/send': 0.17.3 dev: true - /@types/express@4.17.20: - resolution: {integrity: sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==} + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} dependencies: - '@types/body-parser': 1.19.4 - '@types/express-serve-static-core': 4.17.39 + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.17.41 '@types/qs': 6.9.9 '@types/serve-static': 1.15.4 dev: true @@ -3087,41 +2961,34 @@ packages: resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} dev: true - /@types/http-errors@2.0.3: - resolution: {integrity: sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==} + /@types/http-errors@2.0.4: + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} dev: true - /@types/http-proxy@1.17.13: - resolution: {integrity: sha512-GkhdWcMNiR5QSQRYnJ+/oXzu0+7JJEPC8vkWXK351BkhjraZF+1W13CUYARUvX9+NqIU2n6YHA4iwywsc/M6Sw==} + /@types/http-proxy@1.17.14: + resolution: {integrity: sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==} dependencies: '@types/node': 20.8.10 dev: true - /@types/inquirer@9.0.6: - resolution: {integrity: sha512-1Go1AAP/yOy3Pth5Xf1DC3nfZ03cJLCPx6E2YnSN/5I3w1jHBVH4170DkZ+JxfmA7c9kL9+bf9z3FRGa4kNAqg==} - dependencies: - '@types/through': 0.0.32 - rxjs: 7.8.1 - dev: true - - /@types/istanbul-lib-coverage@2.0.5: - resolution: {integrity: sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==} + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} dev: true - /@types/istanbul-lib-report@3.0.2: - resolution: {integrity: sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==} + /@types/istanbul-lib-report@3.0.3: + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} dependencies: - '@types/istanbul-lib-coverage': 2.0.5 + '@types/istanbul-lib-coverage': 2.0.6 dev: true - /@types/istanbul-reports@3.0.3: - resolution: {integrity: sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==} + /@types/istanbul-reports@3.0.4: + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} dependencies: - '@types/istanbul-lib-report': 3.0.2 + '@types/istanbul-lib-report': 3.0.3 dev: true - /@types/json-schema@7.0.14: - resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==} + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} @@ -3181,8 +3048,8 @@ packages: resolution: {integrity: sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==} dev: true - /@types/react@18.2.34: - resolution: {integrity: sha512-U6eW/alrRk37FU/MS2RYMjx0Va2JGIVXELTODaTIYgvWGCV4Y4TfTUzG8DdmpDNIT0Xpj/R7GfyHOJJrDttcvg==} + /@types/react@18.2.36: + resolution: {integrity: sha512-o9XFsHYLLZ4+sb9CWUYwHqFVoG61SesydF353vFMMsQziiyRu8np4n2OYMUSDZ8XuImxDr9c5tR7gidlH29Vnw==} dependencies: '@types/prop-types': 15.7.9 '@types/scheduler': 0.16.5 @@ -3221,13 +3088,13 @@ packages: /@types/serve-index@1.9.3: resolution: {integrity: sha512-4KG+yMEuvDPRrYq5fyVm/I2uqAJSAwZK9VSa+Zf+zUq9/oxSSvy3kkIqyL+jjStv6UCVi8/Aho0NHtB1Fwosrg==} dependencies: - '@types/express': 4.17.20 + '@types/express': 4.17.21 dev: true /@types/serve-static@1.15.4: resolution: {integrity: sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw==} dependencies: - '@types/http-errors': 2.0.3 + '@types/http-errors': 2.0.4 '@types/mime': 3.0.3 '@types/node': 20.8.10 dev: true @@ -3248,22 +3115,10 @@ packages: '@types/node': 20.8.10 dev: true - /@types/through@0.0.32: - resolution: {integrity: sha512-7XsfXIsjdfJM2wFDRAtEWp3zb2aVPk5QeyZxGlVK57q4u26DczMHhJmlhr0Jqv0THwxam/L8REXkj8M2I/lcvw==} - dependencies: - '@types/node': 20.8.10 - dev: true - /@types/unist@2.0.9: resolution: {integrity: sha512-zC0iXxAv1C1ERURduJueYzkzZ2zaGyc+P2c95hgkikHPr3z8EdUZOlgEQ5X0DRmwDZn+hekycQnoeiiRVrmilQ==} dev: true - /@types/varint@6.0.2: - resolution: {integrity: sha512-gBTZgG13ulb81tQwk//LnPwpl8hue2SJ9peL3i8ZVA3ZXU6Y7gT9S7MHcunzDbG07yXiT1+m1LkQjPYb+PCNWQ==} - dependencies: - '@types/node': 20.8.10 - dev: true - /@types/ws@8.5.8: resolution: {integrity: sha512-flUksGIQCnJd6sZ1l5dqCEG/ksaoAg/eUwiLAGTJQcfgvZJKF++Ta4bJA6A5aPSJmsr+xlseHn4KLgVlNnvPTg==} dependencies: @@ -3274,14 +3129,14 @@ packages: resolution: {integrity: sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==} dev: true - /@types/yargs@17.0.29: - resolution: {integrity: sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==} + /@types/yargs@17.0.30: + resolution: {integrity: sha512-3SJLzYk3yz3EgI9I8OLoH06B3PdXIoU2imrBZzaGqUtUXf5iUNDtmAfCGuQrny1bnmyjh/GM/YNts6WK5jR5Rw==} dependencies: '@types/yargs-parser': 21.0.2 dev: true - /@typescript-eslint/eslint-plugin@6.9.1(@typescript-eslint/parser@6.9.1)(eslint@8.52.0)(typescript@5.2.2): - resolution: {integrity: sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==} + /@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha @@ -3292,13 +3147,13 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.9.1(eslint@8.52.0)(typescript@5.2.2) - '@typescript-eslint/scope-manager': 6.9.1 - '@typescript-eslint/type-utils': 6.9.1(eslint@8.52.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.9.1(eslint@8.52.0)(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.9.1 + '@typescript-eslint/parser': 6.10.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 6.10.0 + '@typescript-eslint/type-utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.10.0 debug: 4.3.4(supports-color@8.1.1) - eslint: 8.52.0 + eslint: 8.53.0 graphemer: 1.4.0 ignore: 5.2.4 natural-compare: 1.4.0 @@ -3309,8 +3164,8 @@ packages: - supports-color dev: false - /@typescript-eslint/parser@6.9.1(eslint@8.52.0)(typescript@5.2.2): - resolution: {integrity: sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==} + /@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -3319,27 +3174,27 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.9.1 - '@typescript-eslint/types': 6.9.1 - '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.9.1 + '@typescript-eslint/scope-manager': 6.10.0 + '@typescript-eslint/types': 6.10.0 + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.10.0 debug: 4.3.4(supports-color@8.1.1) - eslint: 8.52.0 + eslint: 8.53.0 typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: false - /@typescript-eslint/scope-manager@6.9.1: - resolution: {integrity: sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==} + /@typescript-eslint/scope-manager@6.10.0: + resolution: {integrity: sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.9.1 - '@typescript-eslint/visitor-keys': 6.9.1 + '@typescript-eslint/types': 6.10.0 + '@typescript-eslint/visitor-keys': 6.10.0 dev: false - /@typescript-eslint/type-utils@6.9.1(eslint@8.52.0)(typescript@5.2.2): - resolution: {integrity: sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==} + /@typescript-eslint/type-utils@6.10.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -3348,23 +3203,23 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) - '@typescript-eslint/utils': 6.9.1(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) + '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) debug: 4.3.4(supports-color@8.1.1) - eslint: 8.52.0 + eslint: 8.53.0 ts-api-utils: 1.0.3(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: false - /@typescript-eslint/types@6.9.1: - resolution: {integrity: sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==} + /@typescript-eslint/types@6.10.0: + resolution: {integrity: sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==} engines: {node: ^16.0.0 || >=18.0.0} dev: false - /@typescript-eslint/typescript-estree@6.9.1(typescript@5.2.2): - resolution: {integrity: sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==} + /@typescript-eslint/typescript-estree@6.10.0(typescript@5.2.2): + resolution: {integrity: sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -3372,8 +3227,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.9.1 - '@typescript-eslint/visitor-keys': 6.9.1 + '@typescript-eslint/types': 6.10.0 + '@typescript-eslint/visitor-keys': 6.10.0 debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 @@ -3384,30 +3239,30 @@ packages: - supports-color dev: false - /@typescript-eslint/utils@6.9.1(eslint@8.52.0)(typescript@5.2.2): - resolution: {integrity: sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==} + /@typescript-eslint/utils@6.10.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) - '@types/json-schema': 7.0.14 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@types/json-schema': 7.0.15 '@types/semver': 7.5.4 - '@typescript-eslint/scope-manager': 6.9.1 - '@typescript-eslint/types': 6.9.1 - '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) - eslint: 8.52.0 + '@typescript-eslint/scope-manager': 6.10.0 + '@typescript-eslint/types': 6.10.0 + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) + eslint: 8.53.0 semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript dev: false - /@typescript-eslint/visitor-keys@6.9.1: - resolution: {integrity: sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==} + /@typescript-eslint/visitor-keys@6.10.0: + resolution: {integrity: sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.9.1 + '@typescript-eslint/types': 6.10.0 eslint-visitor-keys: 3.4.3 dev: false @@ -3472,56 +3327,56 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: false - /@vue/compiler-core@3.3.7: - resolution: {integrity: sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==} + /@vue/compiler-core@3.3.8: + resolution: {integrity: sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==} dependencies: '@babel/parser': 7.23.0 - '@vue/shared': 3.3.7 + '@vue/shared': 3.3.8 estree-walker: 2.0.2 source-map-js: 1.0.2 dev: false - /@vue/compiler-dom@3.3.7: - resolution: {integrity: sha512-0LwkyJjnUPssXv/d1vNJ0PKfBlDoQs7n81CbO6Q0zdL7H1EzqYRrTVXDqdBVqro0aJjo/FOa1qBAPVI4PGSHBw==} + /@vue/compiler-dom@3.3.8: + resolution: {integrity: sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==} dependencies: - '@vue/compiler-core': 3.3.7 - '@vue/shared': 3.3.7 + '@vue/compiler-core': 3.3.8 + '@vue/shared': 3.3.8 dev: false - /@vue/compiler-sfc@3.3.7: - resolution: {integrity: sha512-7pfldWy/J75U/ZyYIXRVqvLRw3vmfxDo2YLMwVtWVNew8Sm8d6wodM+OYFq4ll/UxfqVr0XKiVwti32PCrruAw==} + /@vue/compiler-sfc@3.3.8: + resolution: {integrity: sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA==} dependencies: '@babel/parser': 7.23.0 - '@vue/compiler-core': 3.3.7 - '@vue/compiler-dom': 3.3.7 - '@vue/compiler-ssr': 3.3.7 - '@vue/reactivity-transform': 3.3.7 - '@vue/shared': 3.3.7 + '@vue/compiler-core': 3.3.8 + '@vue/compiler-dom': 3.3.8 + '@vue/compiler-ssr': 3.3.8 + '@vue/reactivity-transform': 3.3.8 + '@vue/shared': 3.3.8 estree-walker: 2.0.2 magic-string: 0.30.5 postcss: 8.4.31 source-map-js: 1.0.2 dev: false - /@vue/compiler-ssr@3.3.7: - resolution: {integrity: sha512-TxOfNVVeH3zgBc82kcUv+emNHo+vKnlRrkv8YvQU5+Y5LJGJwSNzcmLUoxD/dNzv0bhQ/F0s+InlgV0NrApJZg==} + /@vue/compiler-ssr@3.3.8: + resolution: {integrity: sha512-hXCqQL/15kMVDBuoBYpUnSYT8doDNwsjvm3jTefnXr+ytn294ySnT8NlsFHmTgKNjwpuFy7XVV8yTeLtNl/P6w==} dependencies: - '@vue/compiler-dom': 3.3.7 - '@vue/shared': 3.3.7 + '@vue/compiler-dom': 3.3.8 + '@vue/shared': 3.3.8 dev: false - /@vue/reactivity-transform@3.3.7: - resolution: {integrity: sha512-APhRmLVbgE1VPGtoLQoWBJEaQk4V8JUsqrQihImVqKT+8U6Qi3t5ATcg4Y9wGAPb3kIhetpufyZ1RhwbZCIdDA==} + /@vue/reactivity-transform@3.3.8: + resolution: {integrity: sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw==} dependencies: '@babel/parser': 7.23.0 - '@vue/compiler-core': 3.3.7 - '@vue/shared': 3.3.7 + '@vue/compiler-core': 3.3.8 + '@vue/shared': 3.3.8 estree-walker: 2.0.2 magic-string: 0.30.5 dev: false - /@vue/shared@3.3.7: - resolution: {integrity: sha512-N/tbkINRUDExgcPTBvxNkvHGu504k8lzlNQRITVnm6YjOjwa4r0nnbd4Jb01sNpur5hAllyRJzSK5PvB9PPwRg==} + /@vue/shared@3.3.8: + resolution: {integrity: sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==} dev: false /@web-std/blob@3.0.5: @@ -3959,7 +3814,7 @@ packages: postcss: ^8.1.0 dependencies: browserslist: 4.22.1 - caniuse-lite: 1.0.30001559 + caniuse-lite: 1.0.30001561 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -4194,8 +4049,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001559 - electron-to-chromium: 1.4.575 + caniuse-lite: 1.0.30001561 + electron-to-chromium: 1.4.577 node-releases: 2.0.13 update-browserslist-db: 1.0.13(browserslist@4.22.1) dev: true @@ -4240,7 +4095,7 @@ packages: '@istanbuljs/schema': 0.1.3 find-up: 5.0.0 foreground-child: 2.0.0 - istanbul-lib-coverage: 3.2.0 + istanbul-lib-coverage: 3.2.1 istanbul-lib-report: 3.0.1 istanbul-reports: 3.1.6 rimraf: 3.0.2 @@ -4259,7 +4114,7 @@ packages: '@istanbuljs/schema': 0.1.3 find-up: 5.0.0 foreground-child: 2.0.0 - istanbul-lib-coverage: 3.2.0 + istanbul-lib-coverage: 3.2.1 istanbul-lib-report: 3.0.1 istanbul-reports: 3.1.6 rimraf: 3.0.2 @@ -4323,13 +4178,13 @@ packages: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} dependencies: browserslist: 4.22.1 - caniuse-lite: 1.0.30001559 + caniuse-lite: 1.0.30001561 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 dev: true - /caniuse-lite@1.0.30001559: - resolution: {integrity: sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==} + /caniuse-lite@1.0.30001561: + resolution: {integrity: sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==} dev: true /cardinal@2.1.1: @@ -4674,7 +4529,7 @@ packages: peerDependencies: webpack: ^5.1.0 dependencies: - fast-glob: 3.3.1 + fast-glob: 3.3.2 glob-parent: 6.0.2 globby: 13.2.2 normalize-path: 3.0.0 @@ -5080,7 +4935,7 @@ packages: dependencies: '@babel/parser': 7.23.0 '@babel/traverse': 7.23.2 - '@vue/compiler-sfc': 3.3.7 + '@vue/compiler-sfc': 3.3.8 callsite: 1.0.0 camelcase: 6.3.0 cosmiconfig: 7.1.0 @@ -5194,24 +5049,24 @@ packages: esutils: 2.0.3 dev: false - /docusaurus-plugin-typedoc@0.18.0(typedoc-plugin-markdown@3.17.0)(typedoc@0.23.28): + /docusaurus-plugin-typedoc@0.18.0(typedoc-plugin-markdown@3.17.1)(typedoc@0.23.28): resolution: {integrity: sha512-kurIUu8LhVIOPT88HoeBcu0/D2GMDdg0pUYaFlqeuXT9an6Wlgvuy0C22ZMYcJUcp/gA/Mw2XdUHubsLK2M4uA==} peerDependencies: typedoc: '>=0.23.0' typedoc-plugin-markdown: '>=3.13.0' dependencies: typedoc: 0.23.28(typescript@5.2.2) - typedoc-plugin-markdown: 3.17.0(typedoc@0.23.28) + typedoc-plugin-markdown: 3.17.1(typedoc@0.23.28) dev: true - /docusaurus-plugin-typedoc@0.18.0(typedoc-plugin-markdown@3.17.0)(typedoc@0.25.3): + /docusaurus-plugin-typedoc@0.18.0(typedoc-plugin-markdown@3.17.1)(typedoc@0.25.3): resolution: {integrity: sha512-kurIUu8LhVIOPT88HoeBcu0/D2GMDdg0pUYaFlqeuXT9an6Wlgvuy0C22ZMYcJUcp/gA/Mw2XdUHubsLK2M4uA==} peerDependencies: typedoc: '>=0.23.0' typedoc-plugin-markdown: '>=3.13.0' dependencies: typedoc: 0.25.3(typescript@5.2.2) - typedoc-plugin-markdown: 3.17.0(typedoc@0.25.3) + typedoc-plugin-markdown: 3.17.1(typedoc@0.25.3) dev: true /dom-converter@0.2.0: @@ -5291,8 +5146,8 @@ packages: encoding: 0.1.13 dev: false - /electron-to-chromium@1.4.575: - resolution: {integrity: sha512-kY2BGyvgAHiX899oF6xLXSIf99bAvvdPhDoJwG77nxCSyWYuRH6e9a9a3gpXBvCs6lj4dQZJkfnW2hdKWHEISg==} + /electron-to-chromium@1.4.577: + resolution: {integrity: sha512-/5xHPH6f00SxhHw6052r+5S1xO7gHNc89hV7tqlvnStvKbSrDqc/u6AlwPvVWWNj+s4/KL6T6y8ih+nOY0qYNA==} dev: true /emoji-regex@10.3.0: @@ -5490,7 +5345,7 @@ packages: engines: {node: '>=12'} dev: true - /eslint-plugin-jsdoc@46.8.2(eslint@8.52.0): + /eslint-plugin-jsdoc@46.8.2(eslint@8.53.0): resolution: {integrity: sha512-5TSnD018f3tUJNne4s4gDWQflbsgOycIKEUBoCLn6XtBMgNHxQFmV8vVxUtiPxAQq8lrX85OaSG/2gnctxw9uQ==} engines: {node: '>=16'} peerDependencies: @@ -5501,7 +5356,7 @@ packages: comment-parser: 1.4.0 debug: 4.3.4(supports-color@8.1.1) escape-string-regexp: 4.0.0 - eslint: 8.52.0 + eslint: 8.53.0 esquery: 1.5.0 is-builtin-module: 3.2.1 semver: 7.5.4 @@ -5531,15 +5386,15 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: false - /eslint@8.52.0: - resolution: {integrity: sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==} + /eslint@8.53.0: + resolution: {integrity: sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) '@eslint-community/regexpp': 4.10.0 - '@eslint/eslintrc': 2.1.2 - '@eslint/js': 8.52.0 + '@eslint/eslintrc': 2.1.3 + '@eslint/js': 8.53.0 '@humanwhocodes/config-array': 0.11.13 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 @@ -5773,8 +5628,8 @@ packages: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} dev: false - /fast-glob@3.3.1: - resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5962,7 +5817,7 @@ packages: optional: true dependencies: '@babel/code-frame': 7.22.13 - '@types/json-schema': 7.0.14 + '@types/json-schema': 7.0.15 chalk: 4.1.2 chokidar: 3.5.3 cosmiconfig: 6.0.0 @@ -6213,7 +6068,7 @@ packages: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.3.1 + fast-glob: 3.3.2 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 @@ -6223,7 +6078,7 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: dir-glob: 3.0.1 - fast-glob: 3.3.1 + fast-glob: 3.3.2 ignore: 5.2.4 merge2: 1.4.1 slash: 4.0.0 @@ -6541,7 +6396,7 @@ packages: resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} dev: true - /http-proxy-middleware@2.0.6(@types/express@4.17.20): + /http-proxy-middleware@2.0.6(@types/express@4.17.21): resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==} engines: {node: '>=12.0.0'} peerDependencies: @@ -6550,8 +6405,8 @@ packages: '@types/express': optional: true dependencies: - '@types/express': 4.17.20 - '@types/http-proxy': 1.17.13 + '@types/express': 4.17.21 + '@types/http-proxy': 1.17.14 http-proxy: 1.18.1 is-glob: 4.0.3 is-plain-obj: 3.0.0 @@ -6773,7 +6628,7 @@ packages: it-glob: 1.0.2 it-to-stream: 1.0.0 merge-options: 3.0.4 - nanoid: 3.3.6 + nanoid: 3.3.7 native-fetch: 3.0.0(node-fetch@2.7.0) node-fetch: 2.7.0 react-native-fetch-api: 3.0.0 @@ -7128,8 +6983,8 @@ packages: engines: {node: '>=0.10.0'} dev: true - /istanbul-lib-coverage@3.2.0: - resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} + /istanbul-lib-coverage@3.2.1: + resolution: {integrity: sha512-opCrKqbthmq3SKZ10mFMQG9dk3fTa3quaOLD35kJa5ejwZHd9xAr+kLuziiZz2cG32s4lMZxNdmdcEQnTDP4+g==} engines: {node: '>=8'} dev: true @@ -7137,7 +6992,7 @@ packages: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} dependencies: - istanbul-lib-coverage: 3.2.0 + istanbul-lib-coverage: 3.2.1 make-dir: 4.0.0 supports-color: 7.2.0 dev: true @@ -7945,13 +7800,13 @@ packages: hasBin: true dev: true - /nanoid@3.3.6: - resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - /nanoid@5.0.2: - resolution: {integrity: sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg==} + /nanoid@5.0.3: + resolution: {integrity: sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==} engines: {node: ^18 || >=20} hasBin: true dev: true @@ -8554,7 +8409,7 @@ packages: lilconfig: 2.1.0 lodash: 4.17.21 merge-options: 3.0.4 - nanoid: 5.0.2 + nanoid: 5.0.3 ora: 7.0.1 p-timeout: 6.1.2 path-browserify: 1.0.1 @@ -8991,7 +8846,7 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} dependencies: - nanoid: 3.3.6 + nanoid: 3.3.7 picocolors: 1.0.0 source-map-js: 1.0.2 @@ -9635,7 +9490,7 @@ packages: resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==} engines: {node: '>= 8.9.0'} dependencies: - '@types/json-schema': 7.0.14 + '@types/json-schema': 7.0.15 ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) dev: true @@ -9644,7 +9499,7 @@ packages: resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} engines: {node: '>= 8.9.0'} dependencies: - '@types/json-schema': 7.0.14 + '@types/json-schema': 7.0.15 ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) dev: true @@ -9653,7 +9508,7 @@ packages: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/json-schema': 7.0.14 + '@types/json-schema': 7.0.15 ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) dev: true @@ -9662,7 +9517,7 @@ packages: resolution: {integrity: sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==} engines: {node: '>= 12.13.0'} dependencies: - '@types/json-schema': 7.0.14 + '@types/json-schema': 7.0.15 ajv: 8.12.0 ajv-formats: 2.1.1(ajv@8.12.0) ajv-keywords: 5.1.0(ajv@8.12.0) @@ -10437,6 +10292,7 @@ packages: /type-fest@3.13.1: resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} engines: {node: '>=14.16'} + dev: true /type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} @@ -10490,8 +10346,8 @@ packages: is-typedarray: 1.0.0 dev: true - /typedoc-plugin-markdown@3.17.0(typedoc@0.23.28): - resolution: {integrity: sha512-+uh5fHNfNSGdUxae0FWOuJ8Xu9Sl08jkdshOg6dilAqN/ZXmYsUFFDKw70fYfiGxdCLvpUuyr9FYO+WAa2lHeA==} + /typedoc-plugin-markdown@3.17.1(typedoc@0.23.28): + resolution: {integrity: sha512-QzdU3fj0Kzw2XSdoL15ExLASt2WPqD7FbLeaqwT70+XjKyTshBnUlQA5nNREO1C2P8Uen0CDjsBLMsCQ+zd0lw==} peerDependencies: typedoc: '>=0.24.0' dependencies: @@ -10499,8 +10355,8 @@ packages: typedoc: 0.23.28(typescript@5.2.2) dev: true - /typedoc-plugin-markdown@3.17.0(typedoc@0.25.3): - resolution: {integrity: sha512-+uh5fHNfNSGdUxae0FWOuJ8Xu9Sl08jkdshOg6dilAqN/ZXmYsUFFDKw70fYfiGxdCLvpUuyr9FYO+WAa2lHeA==} + /typedoc-plugin-markdown@3.17.1(typedoc@0.25.3): + resolution: {integrity: sha512-QzdU3fj0Kzw2XSdoL15ExLASt2WPqD7FbLeaqwT70+XjKyTshBnUlQA5nNREO1C2P8Uen0CDjsBLMsCQ+zd0lw==} peerDependencies: typedoc: '>=0.24.0' dependencies: @@ -10800,7 +10656,7 @@ packages: engines: {node: '>=10.12.0'} dependencies: '@jridgewell/trace-mapping': 0.3.20 - '@types/istanbul-lib-coverage': 2.0.5 + '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 dev: true @@ -10966,9 +10822,9 @@ packages: webpack-cli: optional: true dependencies: - '@types/bonjour': 3.5.12 - '@types/connect-history-api-fallback': 1.5.2 - '@types/express': 4.17.20 + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.3 + '@types/express': 4.17.21 '@types/serve-index': 1.9.3 '@types/serve-static': 1.15.4 '@types/sockjs': 0.3.35 @@ -10983,7 +10839,7 @@ packages: express: 4.18.2 graceful-fs: 4.2.11 html-entities: 2.4.0 - http-proxy-middleware: 2.0.6(@types/express@4.17.20) + http-proxy-middleware: 2.0.6(@types/express@4.17.21) ipaddr.js: 2.1.0 launch-editor: 2.6.1 open: 8.4.2 @@ -11028,8 +10884,8 @@ packages: webpack-cli: optional: true dependencies: - '@types/eslint-scope': 3.7.6 - '@types/estree': 1.0.4 + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.5 '@webassemblyjs/ast': 1.11.6 '@webassemblyjs/wasm-edit': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6 @@ -11243,11 +11099,11 @@ packages: /yargs-parser@20.2.4: resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} engines: {node: '>=10'} + dev: true /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} - dev: true /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} @@ -11274,7 +11130,7 @@ packages: require-directory: 2.1.1 string-width: 4.2.3 y18n: 5.0.8 - yargs-parser: 20.2.4 + yargs-parser: 20.2.9 /yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}