From b8d53426fcf2c5434b9ebbd7ecf063d84f4831fd Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 29 Apr 2025 13:03:25 +0100 Subject: [PATCH 01/12] feat: allow multiple proofs --- packages/ucn/README.md | 43 ++++++- packages/ucn/src/api.ts | 18 ++- packages/ucn/src/name.js | 200 ++++++++++++++++++++++++++--- packages/ucn/src/revision.js | 18 +-- packages/ucn/test/name.spec.js | 18 +-- packages/ucn/test/revision.spec.js | 6 +- 6 files changed, 257 insertions(+), 46 deletions(-) diff --git a/packages/ucn/README.md b/packages/ucn/README.md index a59279a3b..8e9ce0de6 100644 --- a/packages/ucn/README.md +++ b/packages/ucn/README.md @@ -45,11 +45,10 @@ rendezvous peer is used. ```js import { Name, Agent, Proof } from '@storacha/ucn' -// See "Signing Key and Proof Management" below. +// see "Signing Key and Proof Management" below. const agent = Agent.parse(privateKey) -const proof = Proof.parse(proofYouCan) +const name = Name.parse(agent, nameArchive) -const name = Name.from(agent, proof) const { value } = await Name.resolve(name) console.log(value) @@ -103,6 +102,7 @@ const agent = await Agent.generate() const name = await Name.create(agent) // agent that should be granted access to update the name +// use `agent.did()` to obtain const recipient = DID.parse('did:key:z6Mkve9LRa8nvXx6Gj2GXevZFN5zHb476FZLS7o1q7fJThFV') const proof = await Name.grant(name, recipient, { readOnly: false }) @@ -111,12 +111,27 @@ console.log(await Proof.format(proof)) // e.g. mAYIEAL3bDhGiZXJvb3RzgGd2ZXJzaW9uAbcCAXESIPa/Vl+6QuagDVY... ``` +Using grant: + +```js +import { Agent, Name, Proof } from '@storacha/ucn' + +// the private key that corresponds to `did:key:z6Mkve9LRa8nvXx6Gj2GXevZFN5zHb476FZLS7o1q7fJThFV` +const agent = Agent.parse(process.env.UCN_PRIVATE_KEY) +// the grant created by the other party +const proof = await Proof.parse('mAYIEAL3bDhGiZXJvb3RzgGd2ZXJzaW9uAbcCAXESIPa/Vl+6QuagDVY...') +const name = Name.from(agent, [proof]) + +// ready to use! e.g. +// `const current = await Name.resolve(name)` +``` + ### Signing Key and Proof Management The **agent private key** is the key used to sign UCAN invocations to update the name. -The **proof** is a UCAN delegation from the _name_ to the agent, authorizing it +The **proofs** are UCAN delegations from the _name_ to the agent, authorizing it to read (`clock/head`) and/or mutate (`clock/advance`) the current value. Both of these items MUST be saved if a revision needs to be created in the @@ -125,12 +140,28 @@ future. ```js import fs from 'node:fs' await fs.promises.writeFile('agent.priv', agent.encode()) -await fs.promises.writeFile('proof.ucan', await name.proof.archive()) +await fs.promises.writeFile('name.car', await name.archive()) // or +import { Agent, Name } from '@storacha/ucn' console.log(Agent.format(agent)) // base64 encoded string -console.log(await Proof.format(name.proof)) // base64 encoded string +console.log(await Name.format(name)) // base64 encoded string +``` + +Restoring exising credentials: + +```js +import fs from 'node:fs' +import { Agent, Name } from '@storacha/ucn' + +const agent = Agent.decode(await fs.promises.readFile('agent.priv')) +const name = await Name.extract(agent, await fs.promises.readFile('name.car')) + +// or + +const agent = Agent.parse(process.env.UCN_PRIVATE_KEY) +const name = await Name.parse(agent, process.env.UCN_NAME_ARCHIVE) ``` ### Revision Persistence diff --git a/packages/ucn/src/api.ts b/packages/ucn/src/api.ts index ea7471d11..df1822988 100644 --- a/packages/ucn/src/api.ts +++ b/packages/ucn/src/api.ts @@ -2,8 +2,10 @@ import { ConnectionView, Delegation, DID, + Proof, Principal, Signer, + UCANLink, } from '@ucanto/interface' import { EventLink as ClockEventLink, @@ -20,9 +22,11 @@ export type { ConnectionView, Delegation, DID, + Proof, Principal, Service, Signer, + UCANLink, } export type ClockConnection = ConnectionView> @@ -41,12 +45,24 @@ export interface Name extends Principal { * the agent must be delegated the `clock/head` capability. For write * access the agent must be delegated the `clock/advance` capability. */ - proof: Delegation + proofs: Proof[] /** * Create a delegation allowing the passed receipient to read and/or mutate * the current value of the name. */ grant: (receipent: DID, options?: GrantOptions) => Promise + /** + * Export the name as IPLD blocks. + * + * Note: this does NOT include signer information (the private key). + */ + export: () => AsyncIterable + /** + * Encode the name as a CAR file. + * + * Note: this does NOT include signer information (the private key). + */ + archive: () => Promise } export interface GrantOptions { diff --git a/packages/ucn/src/name.js b/packages/ucn/src/name.js index 90ad72383..e9012c7e6 100644 --- a/packages/ucn/src/name.js +++ b/packages/ucn/src/name.js @@ -1,6 +1,9 @@ -import { delegate } from '@ucanto/core' +import { CAR, CBOR, delegate, Schema, Delegation, sha256, isDelegation } from '@ucanto/core' import { generate } from '@ucanto/principal/ed25519' -import { parse } from '@ipld/dag-ucan/did' +import { parse as parseDID } from '@ipld/dag-ucan/did' +import { create as createLink, parse as parseLink } from 'multiformats/link' +import { identity } from 'multiformats/hashes/identity' +import { base64 } from 'multiformats/bases/base64' import * as ClockCaps from '@web3-storage/clock/capabilities' import { v0, increment, publish, resolve } from './revision.js' @@ -8,20 +11,22 @@ import { v0, increment, publish, resolve } from './revision.js' export { v0, increment, publish, resolve } +const version = 'ucn/name@1.0.0' + +export const ArchiveSchema = Schema.variant({ + [version]: Schema.link({ version: 1 }), +}) + class Name { /** * @param {API.Signer} agent - * @param {API.Delegation} proof + * @param {API.DID} id + * @param {API.Proof[]} proofs */ - constructor(agent, proof) { - if (proof.audience.did() !== agent.did()) { - throw new Error( - `invalid proof: delegation is for ${proof.audience.did()} but agent is ${agent.did()}` - ) - } + constructor(agent, id, proofs) { this.agent = agent - this.id = parse(proof.capabilities[0]?.with) - this.proof = proof + this.id = parseDID(id) + this.proofs = proofs } did() { @@ -36,6 +41,14 @@ class Name { grant(receipient, options) { return grant(this, receipient, options) } + + async * export() { + yield * exportDAG(this) + } + + async archive() { + return archive(this) + } } /** @@ -51,20 +64,156 @@ export const create = async (agent) => { capabilities: [{ can: '*', with: id.did() }], expiration: Infinity, }) - return new Name(agent, proof) + return new Name(agent, id.did(), [proof]) } /** + * Create a name with the passed agent for signing read/write invocations and + * required proofs of access. If the name ID is not provided in options it will + * be derived from the proofs if possible. + * * @param {API.Signer} agent - * @param {API.Delegation} proof + * @param {API.Proof[]} proofs + * @param {object} [options] + * @param {API.DID} [options.id] + * @param {(link: API.UCANLink) => Delegation} [options.resolver] * @returns {API.Name} */ -export const from = (agent, proof) => new Name(agent, proof) +export const from = (agent, proofs, options) => { + let id = options?.id + if (!id) { + // derive ID from delegation + for (const p of proofs) { + if (!isDelegation(p)) continue + if (p.audience.did() !== agent.did()) continue + const cap = p.capabilities.find(c => c.can === '*' || c.can.startsWith('clock/')) + if (!cap || !cap.with.startsWith('did:')) continue + id = parseDID(cap.with).did() + break + } + if (!id) { + throw new Error('could not derive name DID from proofs') + } + } + + // Note: this is not full validation - just a best effort check to ensure the + // we can find a proof that has the right capabilities and match the agent DID + let hasProofLinks = false + let hasProofMatch = false + for (const p of proofs) { + if (!isDelegation(p)) { + hasProofLinks = true + continue + } + const isAudienceMatch = p.audience.did() === agent.did() + if (!isAudienceMatch) continue + const cap = p.capabilities.find(c => c.can === '*' || c.can.startsWith('clock/')) + if (!cap) continue + const isIDMatch = cap.with === id + if (!isIDMatch) continue + hasProofMatch = true + break + } + if (!hasProofMatch && !hasProofLinks) { + throw new Error(`invalid proof: could not find merkle clock proof for agent: ${agent.did()}`) + } + + return new Name(agent, id, proofs) +} + +/** + * @param {API.Name} name + * @returns {AsyncIterableIterator} + */ +export const exportDAG = async function* (name) { + for (const p of name.proofs) { + if (isDelegation(p)) { + yield * p.export() + } + } + const bytes = CBOR.encode({ + id: name.did(), + proofs: name.proofs.map(p => isDelegation(p) ? p.cid : p) + }) + const digest = await sha256.digest(bytes) + const cid = createLink(CBOR.code, digest) + yield { cid, bytes } +} + +/** + * Encode the name as a CAR file. + * + * @param {API.Name} name + * @returns {Promise} + */ +export const archive = async (name) => { + let rootBlock + const blocks = new Map() + for await (const block of name.export()) { + blocks.set(block.cid.toString(), block) + rootBlock = block + } + if (!rootBlock) throw new Error('missing root block') + const variant = await CBOR.write({ [version]: rootBlock.cid }) + return CAR.encode({ roots: [variant], blocks }) +} + +/** + * @param {API.Signer} agent + * @param {Uint8Array} bytes + */ +export const extract = async (agent, bytes) => { + const { roots, blocks } = CAR.decode(bytes) + if (roots.length !== 1) { + throw new Error('unexpected number of roots') + } + + const variant = CBOR.decode(roots[0].bytes) + const [, link] = ArchiveSchema.match(variant) + const rootBlock = blocks.get(String(link)) + if (!rootBlock) { + throw new Error('missing archive root block') + } + + const rootValue = + /** @type {{ id: API.DID, proofs: API.UCANLink[] }} */ + (CBOR.decode(rootBlock.bytes)) + + const proofs = rootValue.proofs.map(p => Delegation.view({ root: p, blocks })) + return new Name(agent, rootValue.id, proofs) +} + +/** @param {API.Revision} revision */ +export const format = async (revision) => { + const bytes = await revision.archive() + const link = createLink(CAR.code, identity.digest(bytes)) + return link.toString(base64) +} + +/** + * @param {API.Signer} agent + * @param {string} str + */ +export const parse = (agent, str) => { + const link = parseLink(str, base64) + if (link.code !== CAR.code) { + throw new Error(`non CAR codec found: 0x${link.code.toString(16)}`) + } + if (link.multihash.code !== identity.code) { + throw new Error( + `non identity multihash: 0x${link.multihash.code.toString(16)}` + ) + } + return extract(agent, link.multihash.digest) +} /** * Create a delegation allowing the passed receipient to read and/or mutate * the current value of the name. * + * Note: if the passed name is _read only_ and proofs contain links then this + * function will NOT error, since resolution happens at invocation time. + * * @param {API.Name} name * @param {API.DID} recipient * @param {API.GrantOptions} [options] @@ -72,10 +221,21 @@ export const from = (agent, proof) => new Name(agent, proof) export const grant = async (name, recipient, options) => { const readOnly = options?.readOnly ?? false if (!readOnly) { - const canWrite = name.proof.capabilities.some((c) => - ['*', ClockCaps.clock.can, ClockCaps.advance.can].includes(c.can) - ) - if (!canWrite) { + // best effort check for writable name + const writeAbilities = ['*', ClockCaps.clock.can, ClockCaps.advance.can] + let hasProofLinks = false + let canWrite = false + for (const p of name.proofs) { + if (!isDelegation(p)) { + hasProofLinks = true + continue + } + canWrite = p.capabilities.some((c) => writeAbilities.includes(c.can)) + if (canWrite) { + break + } + } + if (!hasProofLinks && !canWrite) { throw new Error( `granting write capability: name not writable: delegated capability not found: "${ClockCaps.advance.can}"` ) @@ -83,12 +243,12 @@ export const grant = async (name, recipient, options) => { } return delegate({ issuer: name.agent, - audience: parse(recipient), + audience: parseDID(recipient), capabilities: [ { can: ClockCaps.head.can, with: name.did() }, ...(readOnly ? [] : [{ can: ClockCaps.advance.can, with: name.did() }]), ], - proofs: [name.proof], + proofs: name.proofs, expiration: options?.expiration ?? Infinity, }) } diff --git a/packages/ucn/src/revision.js b/packages/ucn/src/revision.js index 419067a99..edbbfa26c 100644 --- a/packages/ucn/src/revision.js +++ b/packages/ucn/src/revision.js @@ -3,7 +3,7 @@ import { decodeEventBlock, encodeEventBlock, } from '@web3-storage/pail/clock' -import { CAR, CBOR } from '@ucanto/core' +import { CAR, CBOR, Schema } from '@ucanto/core' import { connect } from '@web3-storage/clock/client' import * as ClockCaps from '@web3-storage/clock/capabilities' import { create as createLink, parse as parseLink } from 'multiformats/link' @@ -22,6 +22,10 @@ import * as Value from './value.js' const version = 'ucn/revision@1.0.0' +export const ArchiveSchema = Schema.variant({ + [version]: Schema.link({ version: 1 }), +}) + class Revision { /** @param {API.EventBlockView} event */ constructor(event) { @@ -92,13 +96,11 @@ export const extract = async (bytes) => { } const variant = CBOR.decode(roots[0].bytes) - if (!variant || typeof variant != 'object' || !(version in variant)) { - throw new Error('invalid or unsupported revision') - } + const [, link] = ArchiveSchema.match(variant) - const event = blocks.get(String(variant[version])) + const event = blocks.get(String(link)) if (!event) { - throw new Error('missing revision block') + throw new Error('missing archive root block') } return new Revision(await decodeEventBlock(event.bytes)) @@ -161,7 +163,7 @@ export const publish = async (name, revision, options) => { audience: r.id, with: name.did(), nb: { event: revision.event.cid }, - proofs: [name.proof], + proofs: name.proofs, }) invocation.attach(revision.event) const receipt = await invocation.execute(r) @@ -227,7 +229,7 @@ export const resolve = async (name, options) => { issuer: name.agent, audience: r.id, with: name.did(), - proofs: [name.proof], + proofs: name.proofs, }) const receipt = await invocation.execute(r) if (receipt.out.error) throw receipt.out.error diff --git a/packages/ucn/test/name.spec.js b/packages/ucn/test/name.spec.js index 91107d35a..074f43177 100644 --- a/packages/ucn/test/name.spec.js +++ b/packages/ucn/test/name.spec.js @@ -1,5 +1,5 @@ import { describe, it, assert, expect } from 'vitest' -import { Schema, DID } from '@ucanto/core' +import { Schema, DID, isDelegation } from '@ucanto/core' import * as ed25519 from '@ucanto/principal/ed25519' import * as ClockCaps from '@web3-storage/clock/capabilities' import * as Name from '../src/name.js' @@ -15,11 +15,13 @@ describe('name', () => { const sig = await name.agent.sign(payload) assert(await name.agent.verifier.verify(payload, sig)) - assert.equal(name.proof.issuer.did(), name.did()) - assert.equal(name.proof.audience.did(), name.agent.did()) - assert.equal(name.proof.capabilities[0].can, '*') - assert.equal(name.proof.capabilities[0].with, name.did()) - assert.equal(name.proof.expiration, Infinity) + const proof = name.proofs[0] + assert(isDelegation(proof)) + assert.equal(proof.issuer.did(), name.did()) + assert.equal(proof.audience.did(), name.agent.did()) + assert.equal(proof.capabilities[0].can, '*') + assert.equal(proof.capabilities[0].with, name.did()) + assert.equal(proof.expiration, Infinity) }) it('should create a new name BYO signer', async () => { @@ -68,7 +70,7 @@ describe('name', () => { const receipient1 = await ed25519.generate() const name0 = await Name.create() const proof = await Name.grant(name0, receipient0.did(), { readOnly: true }) - const name1 = Name.from(receipient0, proof) + const name1 = Name.from(receipient0, [proof]) await expect( Name.grant(name1, receipient1.did(), { readOnly: false }) ).rejects.toThrow(/name not writable/) @@ -77,6 +79,6 @@ describe('name', () => { it('should fail to instantiate name for agent and proof mismatch', async () => { const name0 = await Name.create() const name1 = await Name.create() - assert.throws(() => Name.from(name0.agent, name1.proof), /invalid proof/) + assert.throws(() => Name.from(name0.agent, name1.proofs, { id: name1.did() }), /invalid proof/) }) }) diff --git a/packages/ucn/test/revision.spec.js b/packages/ucn/test/revision.spec.js index b291419e8..a67b3d00e 100644 --- a/packages/ucn/test/revision.spec.js +++ b/packages/ucn/test/revision.spec.js @@ -176,7 +176,7 @@ describe('revision', () => { await Revision.publish(name0, rev0, { remotes: [remote] }) const proof = await Name.grant(name0, fixtures.bob.did()) - const name1 = Name.from(fixtures.bob, proof) + const name1 = Name.from(fixtures.bob, [proof]) const res0 = await Revision.resolve(name1, { remotes: [remote] }) assert.equal(res0.value, fixtures.values[0]) @@ -205,7 +205,7 @@ describe('revision', () => { const proof = await Name.grant(name0, fixtures.bob.did(), { readOnly: true, }) - const name1 = Name.from(fixtures.bob, proof) + const name1 = Name.from(fixtures.bob, [proof]) const res0 = await Revision.resolve(name1, { remotes: [remote] }) const rev1 = await Revision.increment(res0, fixtures.values[1]) @@ -233,7 +233,7 @@ describe('revision', () => { assert.equal(pub0.value, fixtures.values[0]) const proof = await Name.grant(name0, fixtures.bob.did()) - const name1 = Name.from(fixtures.bob, proof) + const name1 = Name.from(fixtures.bob, [proof]) // bob publishes on top of rev0 const res0 = await Revision.resolve(name1, { remotes: [remote] }) From b4e865c1e132efe7aa72d3c75012684b22766b8f Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 29 Apr 2025 13:13:03 +0100 Subject: [PATCH 02/12] test: add archive/extract and format/parse tests --- packages/ucn/src/name.js | 6 +++--- packages/ucn/test/name.spec.js | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/ucn/src/name.js b/packages/ucn/src/name.js index e9012c7e6..37f099fac 100644 --- a/packages/ucn/src/name.js +++ b/packages/ucn/src/name.js @@ -183,9 +183,9 @@ export const extract = async (agent, bytes) => { return new Name(agent, rootValue.id, proofs) } -/** @param {API.Revision} revision */ -export const format = async (revision) => { - const bytes = await revision.archive() +/** @param {API.Name} name */ +export const format = async (name) => { + const bytes = await name.archive() const link = createLink(CAR.code, identity.digest(bytes)) return link.toString(base64) } diff --git a/packages/ucn/test/name.spec.js b/packages/ucn/test/name.spec.js index 074f43177..2f94f7b9f 100644 --- a/packages/ucn/test/name.spec.js +++ b/packages/ucn/test/name.spec.js @@ -81,4 +81,26 @@ describe('name', () => { const name1 = await Name.create() assert.throws(() => Name.from(name0.agent, name1.proofs, { id: name1.did() }), /invalid proof/) }) + + it('should roundtrip archive/extract', async () => { + const name = await Name.create() + const bytes = await name.archive() + const extracted = await Name.extract(name.agent, bytes) + assert.equal(extracted.did(), name.did()) + for (const p of name.proofs) { + const proofLink = isDelegation(p) ? p.cid : p + assert(extracted.proofs.some(ep => String(proofLink) === String(isDelegation(ep) ? ep.cid : ep))) + } + }) + + it('should roundtrip format/parse', async () => { + const name = await Name.create() + const str = await Name.format(name) + const extracted = await Name.parse(name.agent, str) + assert.equal(extracted.did(), name.did()) + for (const p of name.proofs) { + const proofLink = isDelegation(p) ? p.cid : p + assert(extracted.proofs.some(ep => String(proofLink) === String(isDelegation(ep) ? ep.cid : ep))) + } + }) }) From 7757db688f7acc64b566433b0ad7a180777bb370 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 29 Apr 2025 13:27:11 +0100 Subject: [PATCH 03/12] fix: cli --- packages/ucn/src/bin/api.ts | 2 +- packages/ucn/src/bin/cmd/grant.js | 7 +++-- packages/ucn/src/bin/cmd/import.js | 4 +-- packages/ucn/src/bin/cmd/list.js | 6 ++--- packages/ucn/src/bin/cmd/remove.js | 11 ++++++-- packages/ucn/src/bin/cmd/resolve.js | 5 ++-- packages/ucn/src/bin/cmd/update.js | 9 ++++--- packages/ucn/src/bin/lib.js | 42 +++++++++++++++-------------- 8 files changed, 48 insertions(+), 38 deletions(-) diff --git a/packages/ucn/src/bin/api.ts b/packages/ucn/src/bin/api.ts index 43734ee96..1448f6d3d 100644 --- a/packages/ucn/src/bin/api.ts +++ b/packages/ucn/src/bin/api.ts @@ -1,2 +1,2 @@ export type { DID, Delegation, Capability } from '@ucanto/interface' -export type { Name, EventLink, Value, Revision } from '../api.js' +export type { Name, EventLink, Value, Proof, Revision, Signer } from '../api.js' diff --git a/packages/ucn/src/bin/cmd/grant.js b/packages/ucn/src/bin/cmd/grant.js index 9d536e97a..5a5d69e21 100644 --- a/packages/ucn/src/bin/cmd/grant.js +++ b/packages/ucn/src/bin/cmd/grant.js @@ -1,6 +1,5 @@ import * as DID from '@ipld/dag-ucan/did' import { getAgent, getNames } from '../lib.js' -import * as Name from '../../name.js' import * as Proof from '../../proof.js' /** @@ -9,14 +8,14 @@ import * as Proof from '../../proof.js' * @param {{ 'read-only'?: boolean }} [options] */ export const handler = async (id, recipient, options) => { - const [agent, names] = await Promise.all([getAgent(), getNames()]) + const agent = await getAgent() + const names = await getNames(agent) const nameID = DID.parse(id).did() if (!names[nameID]) { console.error(`unknown name: ${nameID}`) process.exit(1) } - const name = Name.from(agent, names[nameID]) - const delegation = await name.grant(DID.parse(recipient).did(), { + const delegation = await names[nameID].grant(DID.parse(recipient).did(), { readOnly: options?.['read-only'], }) console.log(await Proof.format(delegation)) diff --git a/packages/ucn/src/bin/cmd/import.js b/packages/ucn/src/bin/cmd/import.js index 03fed6331..dd6d769d6 100644 --- a/packages/ucn/src/bin/cmd/import.js +++ b/packages/ucn/src/bin/cmd/import.js @@ -14,8 +14,8 @@ export const handler = async (b64proof) => { process.exit(1) } - const name = Name.from(agent, proof) + const name = Name.from(agent, [proof]) await addName(name) - console.log(`${isReadOnly(proof) ? 'r-' : 'rw'}\t${name.did()}`) + console.log(`${isReadOnly([proof]) ? 'r-' : 'rw'}\t${name.did()}`) } diff --git a/packages/ucn/src/bin/cmd/list.js b/packages/ucn/src/bin/cmd/list.js index 75fc68428..1cf9ec803 100644 --- a/packages/ucn/src/bin/cmd/list.js +++ b/packages/ucn/src/bin/cmd/list.js @@ -2,13 +2,13 @@ import { getAgent, getNames, isReadOnly } from '../lib.js' /** @param {{ l?: boolean }} [options] */ export const handler = async (options) => { - const [agent, names] = await Promise.all([getAgent(), getNames()]) + const agent = await getAgent() + const names = await getNames(agent) if (options?.l) console.log(`total ${Object.keys(names).length.toLocaleString()}`) for (const [k, v] of Object.entries(names)) { - if (v.audience.did() !== agent.did()) continue if (options?.l) { - console.log(`${isReadOnly(v) ? 'r-' : 'rw'}\t${k}`) + console.log(`${isReadOnly(v.proofs) ? 'r-' : 'rw'}\t${k}`) } else { console.log(k) } diff --git a/packages/ucn/src/bin/cmd/remove.js b/packages/ucn/src/bin/cmd/remove.js index 9a200624b..d87e52fbe 100644 --- a/packages/ucn/src/bin/cmd/remove.js +++ b/packages/ucn/src/bin/cmd/remove.js @@ -1,7 +1,14 @@ import * as DID from '@ipld/dag-ucan/did' -import { removeName } from '../lib.js' +import { getAgent, getNames, removeName } from '../lib.js' /** @param {string} id */ export const handler = async (id) => { - await removeName(DID.parse(id).did()) + const agent = await getAgent() + const names = await getNames(agent) + const nameID = DID.parse(id).did() + if (!names[nameID]) { + console.error(`unknown name: ${nameID}`) + process.exit(1) + } + await removeName(names[nameID]) } diff --git a/packages/ucn/src/bin/cmd/resolve.js b/packages/ucn/src/bin/cmd/resolve.js index 47b466f4d..078e83263 100644 --- a/packages/ucn/src/bin/cmd/resolve.js +++ b/packages/ucn/src/bin/cmd/resolve.js @@ -7,14 +7,15 @@ import * as Name from '../../name.js' * @param {{ local?: boolean }} [options] */ export const handler = async (id, options) => { - const [agent, names] = await Promise.all([getAgent(), getNames()]) + const agent = await getAgent() + const names = await getNames(agent) const nameID = DID.parse(id).did() if (!names[nameID]) { console.error(`unknown name: ${nameID}`) process.exit(1) } - const name = Name.from(agent, names[nameID]) + const name = names[nameID] const base = await getValue(name) let current diff --git a/packages/ucn/src/bin/cmd/update.js b/packages/ucn/src/bin/cmd/update.js index a949244ee..7e010a1a8 100644 --- a/packages/ucn/src/bin/cmd/update.js +++ b/packages/ucn/src/bin/cmd/update.js @@ -14,20 +14,21 @@ import * as Name from '../../name.js' * @param {string} value */ export const handler = async (id, value) => { - const [agent, names] = await Promise.all([getAgent(), getNames()]) + const agent = await getAgent() + const names = await getNames(agent) const nameID = DID.parse(id).did() if (!names[nameID]) { console.error(`unknown name: ${nameID}`) process.exit(1) } - if (isReadOnly(names[nameID])) { + + const name = names[nameID] + if (isReadOnly(name.proofs)) { console.error('unable to update read only name') process.exit(1) } - const name = Name.from(agent, names[nameID]) const base = await getValue(name) - let current try { current = await Name.resolve(name, { base }) diff --git a/packages/ucn/src/bin/lib.js b/packages/ucn/src/bin/lib.js index 481d01aa7..1007ac113 100644 --- a/packages/ucn/src/bin/lib.js +++ b/packages/ucn/src/bin/lib.js @@ -3,11 +3,12 @@ import fs from 'node:fs/promises' import path from 'node:path' import childProcess from 'node:child_process' import * as ed25519 from '@ucanto/principal/ed25519' -import * as Delegation from '@ucanto/core/delegation' import * as dagJSON from '@ipld/dag-json' import * as DID from '@ipld/dag-ucan/did' import * as Revision from '../revision.js' import * as Value from '../value.js' +import { Name } from '../index.js' +import { isDelegation } from '@ucanto/core' /** @import * as API from './api.js' */ @@ -29,8 +30,11 @@ export const getAgent = async () => { return ed25519.decode(data[data['default']]) } -/** @returns {Promise>} */ -export const getNames = async () => { +/** + * @param {API.Signer} agent + * @returns {Promise>} + */ +export const getNames = async (agent) => { const namesPath = path.join(await getDataDir(), 'names.json') let namesBytes try { @@ -40,40 +44,38 @@ export const getNames = async () => { return {} } const data = dagJSON.decode(namesBytes) - /** @type {Record} */ + /** @type {Record} */ const result = {} for (const [k, v] of Object.entries(data)) { - const extracted = await Delegation.extract(v) - if (extracted.error) throw extracted.error - result[DID.parse(k).did()] = extracted.ok + const extracted = await Name.extract(agent, v) + result[DID.parse(k).did()] = extracted } return result } -/** @param {Record} names */ +/** @param {Record} names */ export const setNames = async (names) => { const namesPath = path.join(await getDataDir(), 'names.json') /** @type {Record} */ const data = {} for (const [k, v] of Object.entries(names)) { - const archived = await Delegation.archive(v) - if (archived.error) throw archived.error - data[k] = archived.ok + const archived = await v.archive() + data[k] = archived } await fs.writeFile(namesPath, dagJSON.encode(data), { mode: 0o770 }) } /** @param {API.Name} name */ export const addName = async (name) => { - const names = await getNames() - names[name.did()] = name.proof + const names = await getNames(name.agent) + names[name.did()] = name await setNames(names) } -/** @param {API.DID} id */ -export const removeName = async (id) => { - const names = await getNames() - delete names[id] +/** @param {API.Name} name */ +export const removeName = async (name) => { + const names = await getNames(name.agent) + delete names[name.did()] await setNames(names) } @@ -132,6 +134,6 @@ const getDataDir = async () => { /** @param {API.Capability} c */ const isWritable = (c) => ['*', 'clock/*', 'clock/advance'].includes(c.can) -/** @param {API.Delegation} delegation */ -export const isReadOnly = (delegation) => - !delegation.capabilities.some(isWritable) +/** @param {API.Proof[]} proofs */ +export const isReadOnly = (proofs) => + proofs.some(p => isDelegation(p) && p.capabilities.some(isWritable)) From e7a52e0e4e778e22c9b8ed8591dea1f3e7cb3673 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 29 Apr 2025 13:33:49 +0100 Subject: [PATCH 04/12] chore: appease linter --- packages/ucn/src/api.ts | 4 ++-- packages/ucn/src/bin/lib.js | 2 +- packages/ucn/src/name.js | 36 ++++++++++++++++++++++++---------- packages/ucn/test/name.spec.js | 17 +++++++++++++--- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/packages/ucn/src/api.ts b/packages/ucn/src/api.ts index df1822988..e8357dc2b 100644 --- a/packages/ucn/src/api.ts +++ b/packages/ucn/src/api.ts @@ -53,13 +53,13 @@ export interface Name extends Principal { grant: (receipent: DID, options?: GrantOptions) => Promise /** * Export the name as IPLD blocks. - * + * * Note: this does NOT include signer information (the private key). */ export: () => AsyncIterable /** * Encode the name as a CAR file. - * + * * Note: this does NOT include signer information (the private key). */ archive: () => Promise diff --git a/packages/ucn/src/bin/lib.js b/packages/ucn/src/bin/lib.js index 1007ac113..1003e277f 100644 --- a/packages/ucn/src/bin/lib.js +++ b/packages/ucn/src/bin/lib.js @@ -136,4 +136,4 @@ const isWritable = (c) => ['*', 'clock/*', 'clock/advance'].includes(c.can) /** @param {API.Proof[]} proofs */ export const isReadOnly = (proofs) => - proofs.some(p => isDelegation(p) && p.capabilities.some(isWritable)) + proofs.some((p) => isDelegation(p) && p.capabilities.some(isWritable)) diff --git a/packages/ucn/src/name.js b/packages/ucn/src/name.js index 37f099fac..97e2486c8 100644 --- a/packages/ucn/src/name.js +++ b/packages/ucn/src/name.js @@ -1,4 +1,12 @@ -import { CAR, CBOR, delegate, Schema, Delegation, sha256, isDelegation } from '@ucanto/core' +import { + CAR, + CBOR, + delegate, + Schema, + Delegation, + sha256, + isDelegation, +} from '@ucanto/core' import { generate } from '@ucanto/principal/ed25519' import { parse as parseDID } from '@ipld/dag-ucan/did' import { create as createLink, parse as parseLink } from 'multiformats/link' @@ -42,8 +50,8 @@ class Name { return grant(this, receipient, options) } - async * export() { - yield * exportDAG(this) + async *export() { + yield* exportDAG(this) } async archive() { @@ -86,7 +94,9 @@ export const from = (agent, proofs, options) => { for (const p of proofs) { if (!isDelegation(p)) continue if (p.audience.did() !== agent.did()) continue - const cap = p.capabilities.find(c => c.can === '*' || c.can.startsWith('clock/')) + const cap = p.capabilities.find( + (c) => c.can === '*' || c.can.startsWith('clock/') + ) if (!cap || !cap.with.startsWith('did:')) continue id = parseDID(cap.with).did() break @@ -107,7 +117,9 @@ export const from = (agent, proofs, options) => { } const isAudienceMatch = p.audience.did() === agent.did() if (!isAudienceMatch) continue - const cap = p.capabilities.find(c => c.can === '*' || c.can.startsWith('clock/')) + const cap = p.capabilities.find( + (c) => c.can === '*' || c.can.startsWith('clock/') + ) if (!cap) continue const isIDMatch = cap.with === id if (!isIDMatch) continue @@ -115,7 +127,9 @@ export const from = (agent, proofs, options) => { break } if (!hasProofMatch && !hasProofLinks) { - throw new Error(`invalid proof: could not find merkle clock proof for agent: ${agent.did()}`) + throw new Error( + `invalid proof: could not find merkle clock proof for agent: ${agent.did()}` + ) } return new Name(agent, id, proofs) @@ -128,12 +142,12 @@ export const from = (agent, proofs, options) => { export const exportDAG = async function* (name) { for (const p of name.proofs) { if (isDelegation(p)) { - yield * p.export() + yield* p.export() } } const bytes = CBOR.encode({ id: name.did(), - proofs: name.proofs.map(p => isDelegation(p) ? p.cid : p) + proofs: name.proofs.map((p) => (isDelegation(p) ? p.cid : p)), }) const digest = await sha256.digest(bytes) const cid = createLink(CBOR.code, digest) @@ -175,11 +189,13 @@ export const extract = async (agent, bytes) => { throw new Error('missing archive root block') } - const rootValue = + const rootValue = /** @type {{ id: API.DID, proofs: API.UCANLink[] }} */ (CBOR.decode(rootBlock.bytes)) - const proofs = rootValue.proofs.map(p => Delegation.view({ root: p, blocks })) + const proofs = rootValue.proofs.map((p) => + Delegation.view({ root: p, blocks }) + ) return new Name(agent, rootValue.id, proofs) } diff --git a/packages/ucn/test/name.spec.js b/packages/ucn/test/name.spec.js index 2f94f7b9f..956a428a2 100644 --- a/packages/ucn/test/name.spec.js +++ b/packages/ucn/test/name.spec.js @@ -79,7 +79,10 @@ describe('name', () => { it('should fail to instantiate name for agent and proof mismatch', async () => { const name0 = await Name.create() const name1 = await Name.create() - assert.throws(() => Name.from(name0.agent, name1.proofs, { id: name1.did() }), /invalid proof/) + assert.throws( + () => Name.from(name0.agent, name1.proofs, { id: name1.did() }), + /invalid proof/ + ) }) it('should roundtrip archive/extract', async () => { @@ -89,7 +92,11 @@ describe('name', () => { assert.equal(extracted.did(), name.did()) for (const p of name.proofs) { const proofLink = isDelegation(p) ? p.cid : p - assert(extracted.proofs.some(ep => String(proofLink) === String(isDelegation(ep) ? ep.cid : ep))) + assert( + extracted.proofs.some( + (ep) => String(proofLink) === String(isDelegation(ep) ? ep.cid : ep) + ) + ) } }) @@ -100,7 +107,11 @@ describe('name', () => { assert.equal(extracted.did(), name.did()) for (const p of name.proofs) { const proofLink = isDelegation(p) ? p.cid : p - assert(extracted.proofs.some(ep => String(proofLink) === String(isDelegation(ep) ? ep.cid : ep))) + assert( + extracted.proofs.some( + (ep) => String(proofLink) === String(isDelegation(ep) ? ep.cid : ep) + ) + ) } }) }) From db0760ebfe8217228f006f5ed909135737a182e9 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 30 Apr 2025 10:12:59 +0100 Subject: [PATCH 05/12] fix: ucan:* is also ID match --- packages/ucn/src/name.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ucn/src/name.js b/packages/ucn/src/name.js index 97e2486c8..323876bb1 100644 --- a/packages/ucn/src/name.js +++ b/packages/ucn/src/name.js @@ -121,7 +121,7 @@ export const from = (agent, proofs, options) => { (c) => c.can === '*' || c.can.startsWith('clock/') ) if (!cap) continue - const isIDMatch = cap.with === id + const isIDMatch = cap.with === 'ucan:*' || cap.with === id if (!isIDMatch) continue hasProofMatch = true break From a399a4dd201bb5ca544b63f6474150331b6865f6 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 30 Apr 2025 10:22:17 +0100 Subject: [PATCH 06/12] docs: update jsdoc for Name.from --- packages/ucn/src/name.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/ucn/src/name.js b/packages/ucn/src/name.js index 323876bb1..3395b9d70 100644 --- a/packages/ucn/src/name.js +++ b/packages/ucn/src/name.js @@ -80,11 +80,19 @@ export const create = async (agent) => { * required proofs of access. If the name ID is not provided in options it will * be derived from the proofs if possible. * - * @param {API.Signer} agent - * @param {API.Proof[]} proofs + * Required delegated capabilities: + * - `clock/head` + * + * Optional delegated capabilities: + * - `clock/advance` (required for updates) + * + * @param {API.Signer} agent Signer for invocations to read from or write to the + * merkle clock. + * @param {API.Proof[]} proofs Proof the passed agent can read from + * (`clock/head`) or write to (`clock/advance`) the merkle clock. * @param {object} [options] - * @param {API.DID} [options.id] - * @param {(link: API.UCANLink) => Delegation} [options.resolver] + * @param {API.DID} [options.id] DID of the name. If not provided it will be + * derived from the proofs if possible. * @returns {API.Name} */ export const from = (agent, proofs, options) => { From a3fb5ba3ca63a1a5f10297e2c210001d031387af Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 30 Apr 2025 10:35:21 +0100 Subject: [PATCH 07/12] refactor: interface names --- packages/ucn/src/api.ts | 33 ++++++++++++++++---------------- packages/ucn/src/bin/api.ts | 2 +- packages/ucn/src/bin/lib.js | 20 +++++++++---------- packages/ucn/src/name.js | 14 +++++++------- packages/ucn/src/revision.js | 18 ++++++++--------- packages/ucn/src/server/api.ts | 2 +- packages/ucn/src/server/index.js | 4 ++-- packages/ucn/src/value.js | 20 +++++++++---------- 8 files changed, 56 insertions(+), 57 deletions(-) diff --git a/packages/ucn/src/api.ts b/packages/ucn/src/api.ts index e8357dc2b..bb981e90c 100644 --- a/packages/ucn/src/api.ts +++ b/packages/ucn/src/api.ts @@ -29,13 +29,12 @@ export type { UCANLink, } -export type ClockConnection = ConnectionView> +export type ClockConnection = ConnectionView> /** - * Name is a merkle clock backed, UCAN authorized, mutable reference to a - * resource. + * A merkle clock backed, UCAN authorized, mutable reference to a resource. */ -export interface Name extends Principal { +export interface NameView extends Principal { /** * The agent that signs request to read/update the mutable reference. */ @@ -82,54 +81,54 @@ export interface GrantOptions { * * e.g. /ipfs/bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui */ -export type RawValue = string +export type Value = string /** * A link to a name mutation event. */ -export type EventLink = ClockEventLink +export type EventLink = ClockEventLink /** * A name mutation event. */ -export type EventView = ClockEventView +export type EventView = ClockEventView /** * A name mutation event block. */ -export type EventBlock = Block> +export type EventBlock = Block> /** * A name mutation event block with value. */ -export type EventBlockView = ClockEventBlockView +export type EventBlockView = ClockEventBlockView /** - * Value is the result of resolving the value of one or more revisions. + * The result of resolving the value of one or more revisions. */ -export interface Value { +export interface ValueView { /** * The name the resolved value is associated with. */ - name: Name + name: NameView /** * The resolved value. */ - value: RawValue + value: Value /** * Revision(s) this resolution was calculated from. */ - revision: Revision[] + revision: RevisionView[] } /** - * Revision is a representation of a past, current or future value for a name. + * A representation of a past, current or future value for a name. */ -export interface Revision { +export interface RevisionView { /** * The value associated with this revision. */ - value: RawValue + value: Value /** * The mutation event that backs this revision. */ diff --git a/packages/ucn/src/bin/api.ts b/packages/ucn/src/bin/api.ts index 1448f6d3d..0c2db0afa 100644 --- a/packages/ucn/src/bin/api.ts +++ b/packages/ucn/src/bin/api.ts @@ -1,2 +1,2 @@ export type { DID, Delegation, Capability } from '@ucanto/interface' -export type { Name, EventLink, Value, Proof, Revision, Signer } from '../api.js' +export type { NameView, EventLink, ValueView, Proof, RevisionView, Signer } from '../api.js' diff --git a/packages/ucn/src/bin/lib.js b/packages/ucn/src/bin/lib.js index 1003e277f..b7e1e49a4 100644 --- a/packages/ucn/src/bin/lib.js +++ b/packages/ucn/src/bin/lib.js @@ -32,7 +32,7 @@ export const getAgent = async () => { /** * @param {API.Signer} agent - * @returns {Promise>} + * @returns {Promise>} */ export const getNames = async (agent) => { const namesPath = path.join(await getDataDir(), 'names.json') @@ -44,7 +44,7 @@ export const getNames = async (agent) => { return {} } const data = dagJSON.decode(namesBytes) - /** @type {Record} */ + /** @type {Record} */ const result = {} for (const [k, v] of Object.entries(data)) { const extracted = await Name.extract(agent, v) @@ -53,7 +53,7 @@ export const getNames = async (agent) => { return result } -/** @param {Record} names */ +/** @param {Record} names */ export const setNames = async (names) => { const namesPath = path.join(await getDataDir(), 'names.json') /** @type {Record} */ @@ -65,21 +65,21 @@ export const setNames = async (names) => { await fs.writeFile(namesPath, dagJSON.encode(data), { mode: 0o770 }) } -/** @param {API.Name} name */ +/** @param {API.NameView} name */ export const addName = async (name) => { const names = await getNames(name.agent) names[name.did()] = name await setNames(names) } -/** @param {API.Name} name */ +/** @param {API.NameView} name */ export const removeName = async (name) => { const names = await getNames(name.agent) delete names[name.did()] await setNames(names) } -/** @param {API.Revision} revision */ +/** @param {API.RevisionView} revision */ export const storeRevision = async (revision) => { const bytes = await revision.archive() const tmpPath = path.join(os.tmpdir(), revision.event.cid.toString()) @@ -88,8 +88,8 @@ export const storeRevision = async (revision) => { } /** - * @param {API.Name} name - * @returns {Promise} + * @param {API.NameView} name + * @returns {Promise} */ export const getValue = async (name) => { const valuePath = await getValuePath(name) @@ -108,7 +108,7 @@ export const getValue = async (name) => { return Value.from(name, ...revision) } -/** @param {API.Value} value */ +/** @param {API.ValueView} value */ export const setValue = async (value) => { const valuePath = await getValuePath(value.name) const data = { revision: /** @type {Uint8Array[]} */ ([]) } @@ -118,7 +118,7 @@ export const setValue = async (value) => { return fs.writeFile(valuePath, dagJSON.encode(data), { mode: 0o770 }) } -/** @param {API.Name} name */ +/** @param {API.NameView} name */ const getValuePath = async (name) => { const dir = path.join(await getDataDir(), 'values') await fs.mkdir(dir, { recursive: true, mode: 0o700 }) diff --git a/packages/ucn/src/name.js b/packages/ucn/src/name.js index 3395b9d70..0a78d0ba9 100644 --- a/packages/ucn/src/name.js +++ b/packages/ucn/src/name.js @@ -45,7 +45,7 @@ class Name { return this.did() } - /** @type {API.Name['grant']} */ + /** @type {API.NameView['grant']} */ grant(receipient, options) { return grant(this, receipient, options) } @@ -61,7 +61,7 @@ class Name { /** * @param {API.Signer} [agent] - * @returns {Promise} + * @returns {Promise} */ export const create = async (agent) => { agent = agent ?? (await generate()) @@ -93,7 +93,7 @@ export const create = async (agent) => { * @param {object} [options] * @param {API.DID} [options.id] DID of the name. If not provided it will be * derived from the proofs if possible. - * @returns {API.Name} + * @returns {API.NameView} */ export const from = (agent, proofs, options) => { let id = options?.id @@ -144,7 +144,7 @@ export const from = (agent, proofs, options) => { } /** - * @param {API.Name} name + * @param {API.NameView} name * @returns {AsyncIterableIterator} */ export const exportDAG = async function* (name) { @@ -165,7 +165,7 @@ export const exportDAG = async function* (name) { /** * Encode the name as a CAR file. * - * @param {API.Name} name + * @param {API.NameView} name * @returns {Promise} */ export const archive = async (name) => { @@ -207,7 +207,7 @@ export const extract = async (agent, bytes) => { return new Name(agent, rootValue.id, proofs) } -/** @param {API.Name} name */ +/** @param {API.NameView} name */ export const format = async (name) => { const bytes = await name.archive() const link = createLink(CAR.code, identity.digest(bytes)) @@ -238,7 +238,7 @@ export const parse = (agent, str) => { * Note: if the passed name is _read only_ and proofs contain links then this * function will NOT error, since resolution happens at invocation time. * - * @param {API.Name} name + * @param {API.NameView} name * @param {API.DID} recipient * @param {API.GrantOptions} [options] */ diff --git a/packages/ucn/src/revision.js b/packages/ucn/src/revision.js index edbbfa26c..7ee0cccf2 100644 --- a/packages/ucn/src/revision.js +++ b/packages/ucn/src/revision.js @@ -58,8 +58,8 @@ export const v0 = async (value) => { /** * Create a revision of a previous _value_. * - * @param {API.Value} previous - * @param {API.RawValue} next + * @param {API.ValueView} previous + * @param {API.Value} next */ export const increment = async (previous, next) => { const event = await encodeEventBlock({ @@ -75,7 +75,7 @@ export const from = (event) => new Revision(event) /** * Encode the revision as a CAR file. * - * @param {API.Revision} revision + * @param {API.RevisionView} revision * @returns {Promise} */ export const archive = async (revision) => { @@ -106,7 +106,7 @@ export const extract = async (bytes) => { return new Revision(await decodeEventBlock(event.bytes)) } -/** @param {API.Revision} revision */ +/** @param {API.RevisionView} revision */ export const format = async (revision) => { const bytes = await revision.archive() const link = createLink(CAR.code, identity.digest(bytes)) @@ -133,8 +133,8 @@ export const defaultRemotes = [connect()] * Publish a revision for the passed name to the network. Fails only if the * revision was not able to be published to at least 1 remote. * - * @param {API.Name} name - * @param {API.Revision} revision + * @param {API.NameView} name + * @param {API.RevisionView} revision * @param {object} [options] * @param {API.ClockConnection[]} [options.remotes] * @param {API.BlockFetcher} [options.fetcher] @@ -205,12 +205,12 @@ export const publish = async (name, revision, options) => { * Resolve the current value for the given name. Fails only if no remotes * respond successfully. * - * @param {API.Name} name + * @param {API.NameView} name * @param {object} [options] - * @param {API.Value} [options.base] A known base value to use as the resolution base. + * @param {API.ValueView} [options.base] A known base value to use as the resolution base. * @param {API.ClockConnection[]} [options.remotes] * @param {API.BlockFetcher} [options.fetcher] - * @returns {Promise} + * @returns {Promise} */ export const resolve = async (name, options) => { const remotes = [...(options?.remotes ?? [])] diff --git a/packages/ucn/src/server/api.ts b/packages/ucn/src/server/api.ts index 15f4d221f..2a9fe0340 100644 --- a/packages/ucn/src/server/api.ts +++ b/packages/ucn/src/server/api.ts @@ -3,7 +3,7 @@ import { BlockFetcher, BlockPutter, EventLink } from '../api.js' export type { Service, - RawValue, + Value, EventLink, EventBlock, EventView, diff --git a/packages/ucn/src/server/index.js b/packages/ucn/src/server/index.js index d06bd21e0..2b0f49d6d 100644 --- a/packages/ucn/src/server/index.js +++ b/packages/ucn/src/server/index.js @@ -15,7 +15,7 @@ import { /** * @param {API.Signer} signer - * @param {API.Service} service + * @param {API.Service} service */ export const createServer = (signer, service) => create({ @@ -29,7 +29,7 @@ export const createServer = (signer, service) => /** * @param {API.Context} context - * @returns {API.Service} + * @returns {API.Service} */ // TODO: move to w3clock? export const createService = ({ headStore, blockFetcher, blockCache }) => { diff --git a/packages/ucn/src/value.js b/packages/ucn/src/value.js index b071d4888..3f3afd8a0 100644 --- a/packages/ucn/src/value.js +++ b/packages/ucn/src/value.js @@ -2,9 +2,9 @@ class Value { /** - * @param {API.Name} name - * @param {API.RawValue} value - * @param {API.Revision[]} revision + * @param {API.NameView} name + * @param {API.Value} value + * @param {API.RevisionView[]} revision */ constructor(name, value, revision) { this.name = name @@ -14,18 +14,18 @@ class Value { } /** - * @param {API.Name} name - * @param {API.RawValue} value - * @param {API.Revision[]} revision - * @returns {API.Value} + * @param {API.NameView} name + * @param {API.Value} value + * @param {API.RevisionView[]} revision + * @returns {API.ValueView} */ export const create = (name, value, revision) => new Value(name, value, revision) /** - * @param {API.Name} name - * @param {...API.Revision} revision - * @returns {API.Value} + * @param {API.NameView} name + * @param {...API.RevisionView} revision + * @returns {API.ValueView} */ export const from = (name, ...revision) => { if (!revision.length) throw new Error('missing revisions') From 2e2ec931f8de9560c44e0d12e47498f7a57fcdd9 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 30 Apr 2025 10:53:28 +0100 Subject: [PATCH 08/12] fix: include src in published files for sourcemaps --- packages/ucn/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ucn/package.json b/packages/ucn/package.json index 1f41aa149..7aa8625ae 100644 --- a/packages/ucn/package.json +++ b/packages/ucn/package.json @@ -51,6 +51,7 @@ } }, "files": [ + "src", "dist", "!dist/**/*.js.map" ], From a615e7ae90505ca0a10f3a6497138fc12867e7ba Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 30 Apr 2025 11:45:39 +0100 Subject: [PATCH 09/12] feat: signal no value published with NoValueError --- packages/ucn/README.md | 17 ++++++++++++----- packages/ucn/src/index.js | 1 + packages/ucn/src/revision.js | 11 +++++++++++ packages/ucn/src/server/index.js | 3 +++ packages/ucn/test/revision.spec.js | 17 +++++++++++++++++ 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/ucn/README.md b/packages/ucn/README.md index 8e9ce0de6..aff80c2f8 100644 --- a/packages/ucn/README.md +++ b/packages/ucn/README.md @@ -43,16 +43,23 @@ requests to remote peer(s). If no remote peers are specified, then the Storacha rendezvous peer is used. ```js -import { Name, Agent, Proof } from '@storacha/ucn' +import { Name, Agent, Proof, NoValueError } from '@storacha/ucn' // see "Signing Key and Proof Management" below. const agent = Agent.parse(privateKey) const name = Name.parse(agent, nameArchive) -const { value } = await Name.resolve(name) - -console.log(value) -// e.g. /ipfs/bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui +try { + const { value } = await Name.resolve(name) + + console.log(value) + // e.g. /ipfs/bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui +} catch (err) { + if (err instanceof NoValueError) { + console.log(`No value has been published for ${name}`) + } + throw err +} ``` ### Update diff --git a/packages/ucn/src/index.js b/packages/ucn/src/index.js index eeb162484..84fba2c38 100644 --- a/packages/ucn/src/index.js +++ b/packages/ucn/src/index.js @@ -3,4 +3,5 @@ export * as DID from '@ipld/dag-ucan/did' export * as Name from './name.js' export * as Proof from './proof.js' export * as Revision from './revision.js' +export { NoValueError } from './revision.js' export * as Value from './value.js' diff --git a/packages/ucn/src/revision.js b/packages/ucn/src/revision.js index 7ee0cccf2..587ff2250 100644 --- a/packages/ucn/src/revision.js +++ b/packages/ucn/src/revision.js @@ -204,12 +204,17 @@ export const publish = async (name, revision, options) => { /** * Resolve the current value for the given name. Fails only if no remotes * respond successfully. + * + * If all remotes respond with an empty head, i.e. there is no event published + * to the merkle clock to set the current value then an `NoValueError` is + * thrown, with a `ERR_NO_VALUE` code. * * @param {API.NameView} name * @param {object} [options] * @param {API.ValueView} [options.base] A known base value to use as the resolution base. * @param {API.ClockConnection[]} [options.remotes] * @param {API.BlockFetcher} [options.fetcher] + * @throws {NoValueError} * @returns {Promise} */ export const resolve = async (name, options) => { @@ -242,6 +247,7 @@ export const resolve = async (name, options) => { ) if (!heads.flat().length) { + if (!errors.length) throw new NoValueError(`resolving name: no value`) if (errors.length === 1) throw errors[0] throw new Error('resolving name: no remotes responded successfully', { cause: errors, @@ -266,3 +272,8 @@ export const resolve = async (name, options) => { return Value.from(name, ...revisions) } + +export class NoValueError extends Error { + static code = /** @type {const} */ ('ERR_NO_VALUE') + code = NoValueError.code +} diff --git a/packages/ucn/src/server/index.js b/packages/ucn/src/server/index.js index 2b0f49d6d..2270c5bcb 100644 --- a/packages/ucn/src/server/index.js +++ b/packages/ucn/src/server/index.js @@ -100,6 +100,9 @@ export const createService = ({ headStore, blockFetcher, blockCache }) => { const resource = DID.parse(capability.with).did() const headGet = await headStore.get(resource) if (headGet.error) { + if (headGet.error.name === 'NotFound') { + return ok({ head: [] }) + } return headGet } diff --git a/packages/ucn/test/revision.spec.js b/packages/ucn/test/revision.spec.js index a67b3d00e..f1fda59ed 100644 --- a/packages/ucn/test/revision.spec.js +++ b/packages/ucn/test/revision.spec.js @@ -260,4 +260,21 @@ describe('revision', () => { }) it.skip('should publish to multiple remotes') + + it('should throw when resolving but no value is published', async () => { + const service = createService({ + headStore: new MemoryHeadStorage(), + blockFetcher: { get: async () => undefined }, + blockCache: new MemoryBlockstore(), + }) + + const id = fixtures.service + const server = createServer(id, service) + const remote = connect({ id, codec: CAR.outbound, channel: server }) + const name = await Name.create(fixtures.alice) + + await expect( + Revision.resolve(name, { remotes: [remote] }) + ).rejects.toThrow(/no value/) + }) }) From 3abe67c698e5477e04ddbd7095af049380b424bf Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 30 Apr 2025 11:47:52 +0100 Subject: [PATCH 10/12] docs: use better error test --- packages/ucn/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ucn/README.md b/packages/ucn/README.md index aff80c2f8..3e58f588a 100644 --- a/packages/ucn/README.md +++ b/packages/ucn/README.md @@ -55,7 +55,7 @@ try { console.log(value) // e.g. /ipfs/bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui } catch (err) { - if (err instanceof NoValueError) { + if (err.code === NoValueError.code) { console.log(`No value has been published for ${name}`) } throw err From dae5264a9b12b6f478423fe132827223aea2ed9f Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 30 Apr 2025 14:20:24 +0100 Subject: [PATCH 11/12] chore: appease linter --- packages/ucn/src/bin/api.ts | 9 ++++++++- packages/ucn/src/name.js | 2 +- packages/ucn/src/revision.js | 2 +- packages/ucn/test/revision.spec.js | 6 +++--- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/ucn/src/bin/api.ts b/packages/ucn/src/bin/api.ts index 0c2db0afa..5480d53db 100644 --- a/packages/ucn/src/bin/api.ts +++ b/packages/ucn/src/bin/api.ts @@ -1,2 +1,9 @@ export type { DID, Delegation, Capability } from '@ucanto/interface' -export type { NameView, EventLink, ValueView, Proof, RevisionView, Signer } from '../api.js' +export type { + NameView, + EventLink, + ValueView, + Proof, + RevisionView, + Signer, +} from '../api.js' diff --git a/packages/ucn/src/name.js b/packages/ucn/src/name.js index 0a78d0ba9..e02d04a36 100644 --- a/packages/ucn/src/name.js +++ b/packages/ucn/src/name.js @@ -89,7 +89,7 @@ export const create = async (agent) => { * @param {API.Signer} agent Signer for invocations to read from or write to the * merkle clock. * @param {API.Proof[]} proofs Proof the passed agent can read from - * (`clock/head`) or write to (`clock/advance`) the merkle clock. + * (`clock/head`) or write to (`clock/advance`) the merkle clock. * @param {object} [options] * @param {API.DID} [options.id] DID of the name. If not provided it will be * derived from the proofs if possible. diff --git a/packages/ucn/src/revision.js b/packages/ucn/src/revision.js index 587ff2250..4b934fda0 100644 --- a/packages/ucn/src/revision.js +++ b/packages/ucn/src/revision.js @@ -204,7 +204,7 @@ export const publish = async (name, revision, options) => { /** * Resolve the current value for the given name. Fails only if no remotes * respond successfully. - * + * * If all remotes respond with an empty head, i.e. there is no event published * to the merkle clock to set the current value then an `NoValueError` is * thrown, with a `ERR_NO_VALUE` code. diff --git a/packages/ucn/test/revision.spec.js b/packages/ucn/test/revision.spec.js index f1fda59ed..89bb9d8a4 100644 --- a/packages/ucn/test/revision.spec.js +++ b/packages/ucn/test/revision.spec.js @@ -273,8 +273,8 @@ describe('revision', () => { const remote = connect({ id, codec: CAR.outbound, channel: server }) const name = await Name.create(fixtures.alice) - await expect( - Revision.resolve(name, { remotes: [remote] }) - ).rejects.toThrow(/no value/) + await expect(Revision.resolve(name, { remotes: [remote] })).rejects.toThrow( + /no value/ + ) }) }) From c078b7dbbb685f34fe0160256ee12916eb979070 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Fri, 2 May 2025 15:40:47 +0100 Subject: [PATCH 12/12] feat: add server --- package.json | 10 +++- packages/ucn/src/bin/cmd/server.js | 85 ++++++++++++++++++++++++++++++ packages/ucn/src/bin/index.js | 34 ++++++++++++ packages/ucn/src/bin/lib.js | 2 +- pnpm-lock.yaml | 40 +++++++------- 5 files changed, 148 insertions(+), 23 deletions(-) create mode 100644 packages/ucn/src/bin/cmd/server.js mode change 100644 => 100755 packages/ucn/src/bin/index.js diff --git a/package.json b/package.json index 7d77df4be..3b440ab38 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "packageManager": "pnpm@10.1.0+sha512.c89847b0667ddab50396bbbd008a2a43cf3b581efd59cf5d9aa8923ea1fb4b8106c041d540d08acb095037594d73ebc51e1ec89ee40c88b30b8a66c0fae0ac1b", "scripts": {}, "devDependencies": { - "@nx/next": "20.3.2", "@nx/devkit": "20.3.2", "@nx/js": "catalog:", + "@nx/next": "20.3.2", "@types/npm-registry-fetch": "^8.0.7", "depcheck": "catalog:", "eslint": "catalog:", @@ -32,7 +32,13 @@ "hd-scripts>@typescript-eslint/eslint-plugin": "catalog:", "hd-scripts>@typescript-eslint/parser": "catalog:", "eslint-config-standard-with-typescript>@typescript-eslint/eslint-plugin": "catalog:", - "eslint-config-standard-with-typescript>@typescript-eslint/parser": "catalog:" + "eslint-config-standard-with-typescript>@typescript-eslint/parser": "catalog:", + "ucn": "link:../../../../../Library/pnpm/global/5/node_modules/@storacha/ucn", + "@storacha/ucn": "link:" } + }, + "dependencies": { + "@storacha/ucn": "link:", + "ucn": "link:../../../../../Library/pnpm/global/5/node_modules/@storacha/ucn" } } diff --git a/packages/ucn/src/bin/cmd/server.js b/packages/ucn/src/bin/cmd/server.js new file mode 100644 index 000000000..5192c120f --- /dev/null +++ b/packages/ucn/src/bin/cmd/server.js @@ -0,0 +1,85 @@ +import * as HTTP from 'node:http' +import { Buffer } from 'node:buffer' +import { createService, createServer } from '../../server/index.js' +import { getAgent, getNames, setValue, getValue } from '../lib.js' +import { GatewayBlockFetcher, MemoryBlockstore, withCache } from '../../block.js' +import { Revision, Value } from '../../index.js' +import { error, ok } from '@ucanto/server' +import { decodeEventBlock } from '@web3-storage/pail/clock' + +/** @param {{ p?: string }} [options] */ +export const handler = async (options) => { + const port = options?.p ?? 3000 + + const agent = await getAgent() + const fetcher = withCache(new GatewayBlockFetcher(process.env.UCN_GATEWAY_URL)) + + const nameService = createService({ + headStore: { + // @ts-expect-error + get: async (nameID) => { + const names = await getNames(agent) + const name = names[nameID] + if (!name) { + return error({ + name: /** @type {const} */ ('NotFound'), + message: `Name ${nameID} is not known to this server.` + }) + } + + const value = await getValue(name) + if (!value) { + return error({ + name: /** @type {const} */ ('NotFound'), + message: `Name ${nameID} is not known to this server.` + }) + } + + return ok(value.revision.map(r => ({ event: r.event.cid }))) + }, + put: async (nameID, head) => { + const names = await getNames(agent) + const name = names[nameID] + if (!name) { + return error({ + name: /** @type {const} */ ('NotFound'), + message: `Name ${nameID} is not known to this server.` + }) + } + const revisions = await Promise.all( + head.map(async (h) => { + const block = await fetcher.get(h.event) + if (!block) throw new Error(`fetching event: ${h}`) + return Revision.from(await decodeEventBlock(block.bytes)) + }) + ) + + await setValue(Value.from(name, ...revisions)) + return ok({}) + } + }, + blockFetcher: fetcher, + blockCache: new MemoryBlockstore(), + }) + const ucanServer = createServer(agent, nameService) + + HTTP.createServer(async (request, response) => { + const chunks = [] + for await (const chunk of request) { + chunks.push(chunk) + } + + const { status, headers, body } = await ucanServer.request({ + // @ts-expect-error + headers: request.headers, + body: Buffer.concat(chunks), + }) + + response.writeHead(status ?? 200, headers) + response.write(body) + response.end() + }).listen(port) + + console.log(`Server ID: ${agent.did()}`) + console.log(`Server URL: http://localhost:${port}`) +} diff --git a/packages/ucn/src/bin/index.js b/packages/ucn/src/bin/index.js old mode 100644 new mode 100755 index f19cf8fa0..a2a3216a3 --- a/packages/ucn/src/bin/index.js +++ b/packages/ucn/src/bin/index.js @@ -81,4 +81,38 @@ cli await handler(proof) }) +cli + .command('server') + .describe('Start a UCN server for receiving name updates.') + .option('-p, --port', 'Port to start the server on.', 3000) + .action(async (options) => { + const { handler } = await import('./cmd/server.js') + await handler(options) + }) + +// cli +// .command('remote') +// .alias('remote ls') +// .describe('List configured remotes.') +// .action(async (options) => { +// const { handler } = await import('./cmd/remote/ls.js') +// await handler(options) +// }) + +// cli +// .command('remote add ') +// .describe('Add a remote.') +// .action(async (options) => { +// const { handler } = await import('./cmd/remote/add.js') +// await handler(options) +// }) + +// cli +// .command('remote rm ') +// .describe('Remove a remote.') +// .action(async (options) => { +// const { handler } = await import('./cmd/remote/remove.js') +// await handler(options) +// }) + cli.parse(process.argv) diff --git a/packages/ucn/src/bin/lib.js b/packages/ucn/src/bin/lib.js index b7e1e49a4..3cccbc034 100644 --- a/packages/ucn/src/bin/lib.js +++ b/packages/ucn/src/bin/lib.js @@ -126,7 +126,7 @@ const getValuePath = async (name) => { } const getDataDir = async () => { - const dir = path.join(os.homedir(), '.ucn') + const dir = process.env.UCN_DATA_DIR ?? path.join(os.homedir(), '.ucn') await fs.mkdir(dir, { recursive: true, mode: 0o700 }) return dir } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b86dcbc45..8be73f8fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -297,10 +297,19 @@ overrides: hd-scripts>@typescript-eslint/parser: 8.26.1 eslint-config-standard-with-typescript>@typescript-eslint/eslint-plugin: 8.26.1 eslint-config-standard-with-typescript>@typescript-eslint/parser: 8.26.1 + ucn: link:../../../../../Library/pnpm/global/5/node_modules/@storacha/ucn + '@storacha/ucn': 'link:' importers: .: + dependencies: + '@storacha/ucn': + specifier: 'link:' + version: 'link:' + ucn: + specifier: link:../../../../../Library/pnpm/global/5/node_modules/@storacha/ucn + version: link:../../../../../Library/pnpm/global/5/node_modules/@storacha/ucn devDependencies: '@nx/devkit': specifier: 20.3.2 @@ -310,7 +319,7 @@ importers: version: 20.3.2(@babel/traverse@7.26.5)(@swc/core@1.11.11(@swc/helpers@0.5.15))(@types/node@22.13.10)(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15)))(typescript@5.8.2) '@nx/next': specifier: 20.3.2 - version: 20.3.2(@babel/core@7.26.0)(@babel/traverse@7.26.5)(@rspack/core@1.3.6(@swc/helpers@0.5.15))(@swc/core@1.11.11(@swc/helpers@0.5.15))(@swc/helpers@0.5.15)(@types/node@22.13.10)(@zkochan/js-yaml@0.0.7)(eslint@8.57.1)(next@13.5.11(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.87.0))(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2)(webpack@5.88.0(@swc/core@1.11.11(@swc/helpers@0.5.15))) + version: 20.3.2(@babel/core@7.26.0)(@babel/traverse@7.26.5)(@rspack/core@1.3.6(@swc/helpers@0.5.15))(@swc/core@1.11.11(@swc/helpers@0.5.15))(@swc/helpers@0.5.15)(@types/node@22.13.10)(@zkochan/js-yaml@0.0.7)(eslint@8.57.1)(next@13.5.11(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.87.0))(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2)(webpack@5.99.6(@swc/core@1.11.11(@swc/helpers@0.5.15))) '@types/npm-registry-fetch': specifier: ^8.0.7 version: 8.0.7 @@ -5977,6 +5986,7 @@ packages: '@walletconnect/ethereum-provider@2.9.2': resolution: {integrity: sha512-eO1dkhZffV1g7vpG19XUJTw09M/bwGUwwhy1mJ3AOPbOSbMPvwiCuRz2Kbtm1g9B0Jv15Dl+TvJ9vTgYF8zoZg==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' peerDependencies: '@walletconnect/modal': '>=2' peerDependenciesMeta: @@ -6032,7 +6042,7 @@ packages: '@walletconnect/sign-client@2.9.2': resolution: {integrity: sha512-anRwnXKlR08lYllFMEarS01hp1gr6Q9XUgvacr749hoaC/AwGVlxYFdM8+MyYr3ozlA+2i599kjbK/mAebqdXg==} - deprecated: Reliability and performance greatly improved - please see https://github.com/WalletConnect/walletconnect-monorepo/releases + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/time@1.0.2': resolution: {integrity: sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==} @@ -16865,19 +16875,19 @@ snapshots: - vue-tsc - webpack-cli - '@nx/next@20.3.2(@babel/core@7.26.0)(@babel/traverse@7.26.5)(@rspack/core@1.3.6(@swc/helpers@0.5.15))(@swc/core@1.11.11(@swc/helpers@0.5.15))(@swc/helpers@0.5.15)(@types/node@22.13.10)(@zkochan/js-yaml@0.0.7)(eslint@8.57.1)(next@13.5.11(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.87.0))(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2)(webpack@5.88.0(@swc/core@1.11.11(@swc/helpers@0.5.15)))': + '@nx/next@20.3.2(@babel/core@7.26.0)(@babel/traverse@7.26.5)(@rspack/core@1.3.6(@swc/helpers@0.5.15))(@swc/core@1.11.11(@swc/helpers@0.5.15))(@swc/helpers@0.5.15)(@types/node@22.13.10)(@zkochan/js-yaml@0.0.7)(eslint@8.57.1)(next@13.5.11(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.87.0))(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2)(webpack@5.99.6(@swc/core@1.11.11(@swc/helpers@0.5.15)))': dependencies: '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.26.0) '@nx/devkit': 20.3.2(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15))) '@nx/eslint': 20.3.2(@babel/traverse@7.26.5)(@swc/core@1.11.11(@swc/helpers@0.5.15))(@types/node@22.13.10)(@zkochan/js-yaml@0.0.7)(eslint@8.57.1)(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15))) '@nx/js': 20.3.2(@babel/traverse@7.26.5)(@swc/core@1.11.11(@swc/helpers@0.5.15))(@types/node@22.13.10)(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15)))(typescript@5.8.2) - '@nx/react': 20.3.2(@babel/traverse@7.26.5)(@swc/core@1.11.11(@swc/helpers@0.5.15))(@swc/helpers@0.5.15)(@types/node@22.13.10)(@zkochan/js-yaml@0.0.7)(eslint@8.57.1)(next@13.5.11(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.87.0))(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2)(webpack@5.88.0(@swc/core@1.11.11(@swc/helpers@0.5.15))) + '@nx/react': 20.3.2(@babel/traverse@7.26.5)(@swc/core@1.11.11(@swc/helpers@0.5.15))(@swc/helpers@0.5.15)(@types/node@22.13.10)(@zkochan/js-yaml@0.0.7)(eslint@8.57.1)(next@13.5.11(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.87.0))(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2)(webpack@5.99.6(@swc/core@1.11.11(@swc/helpers@0.5.15))) '@nx/web': 20.3.2(@babel/traverse@7.26.5)(@swc/core@1.11.11(@swc/helpers@0.5.15))(@types/node@22.13.10)(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15)))(typescript@5.8.2) '@nx/webpack': 20.3.2(@babel/traverse@7.26.5)(@rspack/core@1.3.6(@swc/helpers@0.5.15))(@swc/core@1.11.11(@swc/helpers@0.5.15))(@types/node@22.13.10)(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15)))(typescript@5.8.2) '@phenomnomnominal/tsquery': 5.0.1(typescript@5.8.2) '@svgr/webpack': 8.1.0(typescript@5.8.2) - copy-webpack-plugin: 10.2.4(webpack@5.88.0(@swc/core@1.11.11(@swc/helpers@0.5.15))) - file-loader: 6.2.0(webpack@5.88.0(@swc/core@1.11.11(@swc/helpers@0.5.15))) + copy-webpack-plugin: 10.2.4(webpack@5.99.6(@swc/core@1.11.11(@swc/helpers@0.5.15))) + file-loader: 6.2.0(webpack@5.99.6(@swc/core@1.11.11(@swc/helpers@0.5.15))) ignore: 5.3.2 next: 13.5.11(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.87.0) semver: 7.6.3 @@ -16949,7 +16959,7 @@ snapshots: '@nx/nx-win32-x64-msvc@20.3.2': optional: true - '@nx/react@20.3.2(@babel/traverse@7.26.5)(@swc/core@1.11.11(@swc/helpers@0.5.15))(@swc/helpers@0.5.15)(@types/node@22.13.10)(@zkochan/js-yaml@0.0.7)(eslint@8.57.1)(next@13.5.11(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.87.0))(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2)(webpack@5.88.0(@swc/core@1.11.11(@swc/helpers@0.5.15)))': + '@nx/react@20.3.2(@babel/traverse@7.26.5)(@swc/core@1.11.11(@swc/helpers@0.5.15))(@swc/helpers@0.5.15)(@types/node@22.13.10)(@zkochan/js-yaml@0.0.7)(eslint@8.57.1)(next@13.5.11(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.87.0))(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2)(webpack@5.99.6(@swc/core@1.11.11(@swc/helpers@0.5.15)))': dependencies: '@nx/devkit': 20.3.2(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15))) '@nx/eslint': 20.3.2(@babel/traverse@7.26.5)(@swc/core@1.11.11(@swc/helpers@0.5.15))(@types/node@22.13.10)(@zkochan/js-yaml@0.0.7)(eslint@8.57.1)(nx@20.3.2(@swc/core@1.11.11(@swc/helpers@0.5.15))) @@ -16959,7 +16969,7 @@ snapshots: '@phenomnomnominal/tsquery': 5.0.1(typescript@5.8.2) '@svgr/webpack': 8.1.0(typescript@5.8.2) express: 4.21.2 - file-loader: 6.2.0(webpack@5.88.0(@swc/core@1.11.11(@swc/helpers@0.5.15))) + file-loader: 6.2.0(webpack@5.99.6(@swc/core@1.11.11(@swc/helpers@0.5.15))) http-proxy-middleware: 3.0.5 minimatch: 9.0.3 picocolors: 1.1.1 @@ -20989,16 +20999,6 @@ snapshots: graceful-fs: 4.2.11 p-event: 6.0.1 - copy-webpack-plugin@10.2.4(webpack@5.88.0(@swc/core@1.11.11(@swc/helpers@0.5.15))): - dependencies: - fast-glob: 3.3.3 - glob-parent: 6.0.2 - globby: 12.2.0 - normalize-path: 3.0.0 - schema-utils: 4.3.2 - serialize-javascript: 6.0.2 - webpack: 5.88.0(@swc/core@1.11.11(@swc/helpers@0.5.15)) - copy-webpack-plugin@10.2.4(webpack@5.99.6(@swc/core@1.11.11(@swc/helpers@0.5.15))): dependencies: fast-glob: 3.3.3 @@ -22662,11 +22662,11 @@ snapshots: dependencies: flat-cache: 3.2.0 - file-loader@6.2.0(webpack@5.88.0(@swc/core@1.11.11(@swc/helpers@0.5.15))): + file-loader@6.2.0(webpack@5.99.6(@swc/core@1.11.11(@swc/helpers@0.5.15))): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.88.0(@swc/core@1.11.11(@swc/helpers@0.5.15)) + webpack: 5.99.6(@swc/core@1.11.11(@swc/helpers@0.5.15)) file-uri-to-path@1.0.0: {}