From 464acc6a9332ae2744d980402ebb51bf21da8ce1 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 10:37:44 +0100 Subject: [PATCH 01/18] feat: build ts/packages/core The TypeScript implementation of wire-mesh's protocol: ports/adapters architecture per the org's portable-runtime-and-storage-boundaries rules. Transport (length-prefixed CBOR frames over a connection), Storage (async key/value), Identity (device-id derivation, COSE signing/verification), and Clock are first-class port contracts; domain logic depends only on these and the generated schemas, never on a specific adapter's own imports. src/generated/protocol.ts is committed, machine-generated output (generate.ts, run via `pnpm generate`) from cddl.js against ../../../spec/protocol.cddl -- consumed as a git dependency (github:ExaDev/cddl.js#main), since it isn't published to npm. Domain logic is real for the two families the build-out plan names explicitly: handshake negotiation (protocol-version and capability-domain negotiation between two peers -- the mechanism agent-comms issue #31's fix depends on) and capability-token verification (COSE_Sign1 signature check, the self-certifying issuer-key rule, expiry/not-before/revocation, and recursive delegation-chain narrowing). Every other frame family is covered by schema validation only, not yet dispatch/session logic -- see the package's own README for what's deferred and why. Adapters: a Node net.Socket-based TCP transport matching Cascade's own transport shape, an in-memory KeyValueStorage for tests and single-process nodes, a Web Crypto identity (ECDSA P-256 and Ed25519, the two algorithms the spec's conformance vectors use), and the system wall clock. --- ts/package.json | 16 + ts/packages/core/README.md | 28 + ts/packages/core/eslint.config.ts | 28 + ts/packages/core/generate.ts | 18 + ts/packages/core/package.json | 95 + .../core/src/adapters/memory-storage.ts | 18 + .../core/src/adapters/node-identity.ts | 93 + ts/packages/core/src/adapters/system-clock.ts | 8 + .../core/src/adapters/tcp-transport.ts | 143 + ts/packages/core/src/domain/handshake.ts | 34 + ts/packages/core/src/domain/tokens.ts | 133 + ts/packages/core/src/generated/protocol.ts | 350 +++ ts/packages/core/src/generated/runtime.ts | 2 + ts/packages/core/src/ports/clock.ts | 5 + ts/packages/core/src/ports/identity.ts | 19 + ts/packages/core/src/ports/storage.ts | 8 + ts/packages/core/src/ports/transport.ts | 20 + ts/packages/core/test/conformance.test.ts | 85 + ts/packages/core/test/handshake.test.ts | 45 + ts/packages/core/test/tokens.test.ts | 320 ++ ts/packages/core/tsconfig.json | 29 + ts/packages/core/tsconfig.node.json | 7 + ts/packages/core/tsdown.config.ts | 24 + ts/packages/core/turbo.json | 31 + ts/pnpm-lock.yaml | 2579 +++++++++++++++++ ts/pnpm-workspace.yaml | 6 + ts/turbo.json | 24 + 27 files changed, 4168 insertions(+) create mode 100644 ts/package.json create mode 100644 ts/packages/core/README.md create mode 100644 ts/packages/core/eslint.config.ts create mode 100644 ts/packages/core/generate.ts create mode 100644 ts/packages/core/package.json create mode 100644 ts/packages/core/src/adapters/memory-storage.ts create mode 100644 ts/packages/core/src/adapters/node-identity.ts create mode 100644 ts/packages/core/src/adapters/system-clock.ts create mode 100644 ts/packages/core/src/adapters/tcp-transport.ts create mode 100644 ts/packages/core/src/domain/handshake.ts create mode 100644 ts/packages/core/src/domain/tokens.ts create mode 100644 ts/packages/core/src/generated/protocol.ts create mode 100644 ts/packages/core/src/generated/runtime.ts create mode 100644 ts/packages/core/src/ports/clock.ts create mode 100644 ts/packages/core/src/ports/identity.ts create mode 100644 ts/packages/core/src/ports/storage.ts create mode 100644 ts/packages/core/src/ports/transport.ts create mode 100644 ts/packages/core/test/conformance.test.ts create mode 100644 ts/packages/core/test/handshake.test.ts create mode 100644 ts/packages/core/test/tokens.test.ts create mode 100644 ts/packages/core/tsconfig.json create mode 100644 ts/packages/core/tsconfig.node.json create mode 100644 ts/packages/core/tsdown.config.ts create mode 100644 ts/packages/core/turbo.json create mode 100644 ts/pnpm-lock.yaml create mode 100644 ts/pnpm-workspace.yaml create mode 100644 ts/turbo.json diff --git a/ts/package.json b/ts/package.json new file mode 100644 index 0000000..0397898 --- /dev/null +++ b/ts/package.json @@ -0,0 +1,16 @@ +{ + "name": "wire-mesh-ts", + "version": "0.0.0", + "private": true, + "packageManager": "pnpm@10.33.0", + "scripts": { + "build": "turbo run _build", + "test": "turbo run _test", + "typecheck": "turbo run _typecheck", + "lint": "turbo run _lint", + "conformance-check": "turbo run _conformance-check" + }, + "devDependencies": { + "turbo": "2.10.12" + } +} diff --git a/ts/packages/core/README.md b/ts/packages/core/README.md new file mode 100644 index 0000000..f25f1fc --- /dev/null +++ b/ts/packages/core/README.md @@ -0,0 +1,28 @@ +# @exadev/wire-mesh-core + +The TypeScript implementation of wire-mesh's protocol, built ports/adapters: domain logic (`src/domain/`) depends only on port contracts (`src/ports/`) and the [cddl.js](https://github.com/ExaDev/cddl.js)-generated Zod schemas (`src/generated/protocol.ts`, regenerated from `../../../spec/protocol.cddl` via `generate.ts` -- never edited by hand), never on a specific adapter's own imports. + +## Ports + +- **Transport** (`src/ports/transport.ts`) -- an async contract for sending/receiving `Frame` values over a connection. Adapter: `src/adapters/tcp-transport.ts` (length-prefixed CBOR frames over plain `node:net`, matching Cascade's own transport shape). +- **Storage** (`src/ports/storage.ts`) -- an async key/value contract. Adapter: `src/adapters/memory-storage.ts` (in-process, for tests and single-process nodes). +- **Identity** (`src/ports/identity.ts`) -- device-id derivation and COSE signing/verification. Adapter: `src/adapters/node-identity.ts` (Web Crypto, ECDSA P-256 and Ed25519 -- the two algorithms the spec's own conformance vectors use). +- **Clock** (`src/ports/clock.ts`) -- injected time, never `Date.now()` pulled directly into domain logic. Adapter: `src/adapters/system-clock.ts`. + +## Domain logic implemented + +- **Handshake negotiation** (`src/domain/handshake.ts`) -- protocol-version and capability-domain negotiation between two peers, the mechanism agent-comms issue #31 is fixed by. +- **Capability-token verification** (`src/domain/tokens.ts`) -- the full chain tokens.cddl documents: COSE_Sign1 signature verification, the self-certifying issuer-key check (`sha256(issuer-key.public-key) == issuer`), expiry/not-before, revocation, and recursive delegation-chain narrowing (a delegated token's issuer must be its parent's bearer, and its expiry must not exceed its parent's). + +## Deliberately deferred + +Every other frame family (management/exec, streaming, data-domain, federation) is covered by schema validation only -- `conformance-check` proves the generated schemas decode and re-encode every golden vector byte-exactly, including these families, but no domain-level business logic (dispatch, session bookkeeping, PTY/proc lifecycle, oplog replication, federation-link state) exists for them yet. This matches the build-out plan's own stated option to land "transport + handshake + tokens first... with streaming/exec/federation behind later milestones" -- the conformance suite covers all families either way, so nothing here is unverified, only unimplemented. + +## Regenerating the schema + +```sh +pnpm generate # rewrites src/generated/protocol.ts from ../../../spec/protocol.cddl +pnpm test # confirms it still round-trips every conformance vector +``` + +CI regenerates and diffs against the committed file, the same way `conformance/`'s own vector files are verified never to drift from hand-editing. diff --git a/ts/packages/core/eslint.config.ts b/ts/packages/core/eslint.config.ts new file mode 100644 index 0000000..69312ad --- /dev/null +++ b/ts/packages/core/eslint.config.ts @@ -0,0 +1,28 @@ +import { exadevConfig } from "@exadev/eslint-config"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import globals from "globals"; + +export default exadevConfig( + {}, + { + ignores: ["dist", "coverage", "node_modules", ".turbo", "src/generated"], + }, + { + languageOptions: { + parserOptions: { + project: ["./tsconfig.json", "./tsconfig.node.json"], + tsconfigRootDir: import.meta.dirname, + }, + globals: { ...globals.node }, + }, + }, + { + rules: { + "@typescript-eslint/consistent-type-imports": [ + "error", + { fixStyle: "inline-type-imports" }, + ], + }, + }, + eslintPluginPrettierRecommended, +); diff --git a/ts/packages/core/generate.ts b/ts/packages/core/generate.ts new file mode 100644 index 0000000..f6f28b8 --- /dev/null +++ b/ts/packages/core/generate.ts @@ -0,0 +1,18 @@ +// Produces src/generated/protocol.ts from ../../../spec/protocol.cddl via cddl.js. Run `pnpm generate` after the spec changes, then `pnpm test` to confirm generated schemas still round-trip conformance/'s golden vectors. CI regenerates and diffs against the committed file (see .github/workflows/ci.yml's core-verify job) so protocol.ts is never edited by hand. + +import { writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { parse } from "cddl.js/parse"; +import { emitModule } from "cddl.js/emitter"; + +const specPath = fileURLToPath( + new URL("../../../spec/protocol.cddl", import.meta.url), +); +const outputPath = fileURLToPath( + new URL("src/generated/protocol.ts", import.meta.url), +); + +const parsed = parse(specPath); +const source = emitModule(parsed); +writeFileSync(outputPath, source); +console.log(`wrote ${outputPath}`); diff --git a/ts/packages/core/package.json b/ts/packages/core/package.json new file mode 100644 index 0000000..3ad2581 --- /dev/null +++ b/ts/packages/core/package.json @@ -0,0 +1,95 @@ +{ + "name": "@exadev/wire-mesh-core", + "version": "0.0.0", + "private": true, + "type": "module", + "packageManager": "pnpm@10.33.0", + "files": [ + "dist" + ], + "scripts": { + "build": "turbo run _build", + "_build": "tsdown", + "generate": "turbo run _generate", + "_generate": "node generate.ts", + "test": "turbo run _test", + "_test": "vitest run", + "typecheck": "turbo run _typecheck", + "_typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.node.json --noEmit", + "lint": "turbo run _lint", + "_lint": "eslint . --fix --cache --max-warnings 0", + "conformance-check": "turbo run _conformance-check", + "_conformance-check": "vitest run test/conformance.test.ts" + }, + "dependencies": { + "cbor2": "2.3.0", + "cddl.js": "github:ExaDev/cddl.js#main", + "zod": "4.5.4" + }, + "devDependencies": { + "@arethetypeswrong/cli": "0.18.5", + "@exadev/eslint-config": "2.10.6", + "@types/node": "26.4.1", + "eslint": "10.10.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.6", + "globals": "17.12.0", + "jiti": "2.7.0", + "prettier": "3.9.6", + "tsdown": "0.23.0", + "turbo": "2.10.12", + "typescript": "6.0.3", + "vitest": "5.0.0" + }, + "exports": { + "./adapters/memory-storage": { + "import": "./dist/adapters/memory-storage.mjs", + "require": "./dist/adapters/memory-storage.cjs" + }, + "./adapters/node-identity": { + "import": "./dist/adapters/node-identity.mjs", + "require": "./dist/adapters/node-identity.cjs" + }, + "./adapters/system-clock": { + "import": "./dist/adapters/system-clock.mjs", + "require": "./dist/adapters/system-clock.cjs" + }, + "./adapters/tcp-transport": { + "import": "./dist/adapters/tcp-transport.mjs", + "require": "./dist/adapters/tcp-transport.cjs" + }, + "./domain/handshake": { + "import": "./dist/domain/handshake.mjs", + "require": "./dist/domain/handshake.cjs" + }, + "./domain/tokens": { + "import": "./dist/domain/tokens.mjs", + "require": "./dist/domain/tokens.cjs" + }, + "./generated/protocol": { + "import": "./dist/generated/protocol.mjs", + "require": "./dist/generated/protocol.cjs" + }, + "./generated/runtime": { + "import": "./dist/generated/runtime.mjs", + "require": "./dist/generated/runtime.cjs" + }, + "./ports/clock": { + "import": "./dist/ports/clock.mjs", + "require": "./dist/ports/clock.cjs" + }, + "./ports/identity": { + "import": "./dist/ports/identity.mjs", + "require": "./dist/ports/identity.cjs" + }, + "./ports/storage": { + "import": "./dist/ports/storage.mjs", + "require": "./dist/ports/storage.cjs" + }, + "./ports/transport": { + "import": "./dist/ports/transport.mjs", + "require": "./dist/ports/transport.cjs" + }, + "./package.json": "./package.json" + } +} diff --git a/ts/packages/core/src/adapters/memory-storage.ts b/ts/packages/core/src/adapters/memory-storage.ts new file mode 100644 index 0000000..1374357 --- /dev/null +++ b/ts/packages/core/src/adapters/memory-storage.ts @@ -0,0 +1,18 @@ +import type { KeyValueStorage } from "../ports/storage.js"; + +/** An in-process KeyValueStorage backed by a Map -- for tests and single-process nodes; a real deployment substitutes a persistent adapter behind the same contract without touching anything that depends on the port. Not declared `async`: every operation is genuinely synchronous under the hood, so the contract's Promise return is satisfied directly via Promise.resolve() rather than an async function with no await in its body. */ +export function createMemoryStorage(): KeyValueStorage { + const store = new Map(); + + return { + get: async (key) => Promise.resolve(store.get(key)), + set: async (key, value) => + Promise.resolve(store.set(key, value)).then(() => undefined), + delete: async (key) => + Promise.resolve(store.delete(key)).then(() => undefined), + keys: async (prefix) => + Promise.resolve( + [...store.keys()].filter((key) => key.startsWith(prefix)), + ), + }; +} diff --git a/ts/packages/core/src/adapters/node-identity.ts b/ts/packages/core/src/adapters/node-identity.ts new file mode 100644 index 0000000..335d3d1 --- /dev/null +++ b/ts/packages/core/src/adapters/node-identity.ts @@ -0,0 +1,93 @@ +import { webcrypto } from "node:crypto"; +import type { DeviceId, IdentityKey } from "../generated/protocol.js"; +import type { IdentityPort } from "../ports/identity.js"; + +const ES256 = -7; +const EDDSA = -8; + +function algParams(alg: number): EcdsaParams | { name: "Ed25519" } { + if (alg === ES256) { + return { name: "ECDSA", hash: "SHA-256" }; + } + if (alg === EDDSA) { + return { name: "Ed25519" }; + } + throw new Error(`unsupported identity-key alg ${String(alg)}`); +} + +/** Copies into a fresh, non-shared, whole-buffer Uint8Array -- Web Crypto's BufferSource parameters reject a view over a SharedArrayBuffer or a sub-range view, neither of which a caller-supplied Uint8Array is guaranteed not to be. */ +function toBufferSource(bytes: Uint8Array): Uint8Array { + return Uint8Array.from(bytes); +} + +async function importPublicKey(key: IdentityKey): Promise { + if (key.alg === ES256) { + return webcrypto.subtle.importKey( + "raw", + toBufferSource(key["public-key"]), + { name: "ECDSA", namedCurve: "P-256" }, + false, + ["verify"], + ); + } + if (key.alg === EDDSA) { + return webcrypto.subtle.importKey( + "raw", + toBufferSource(key["public-key"]), + { name: "Ed25519" }, + false, + ["verify"], + ); + } + throw new Error(`unsupported identity-key alg ${String(key.alg)}`); +} + +export async function deriveDeviceId(publicKey: Uint8Array): Promise { + return new Uint8Array( + await webcrypto.subtle.digest("SHA-256", toBufferSource(publicKey)), + ); +} + +export async function verifyWithPublicKey( + key: IdentityKey, + message: Uint8Array, + signature: Uint8Array, +): Promise { + const cryptoKey = await importPublicKey(key); + return webcrypto.subtle.verify( + algParams(key.alg), + cryptoKey, + toBufferSource(signature), + toBufferSource(message), + ); +} + +/** + * Builds an IdentityPort from a Web Crypto private key already generated for this node -- ECDSA P-256 (alg -7) or Ed25519 (alg -8), the two algorithms identity-key.alg actually carries in the spec's own conformance vectors. Device-id derivation and signature verification are pure functions of the given key bytes (exported separately above), so they work for an arbitrary issuer-key too, not just this node's own. + */ +export async function createNodeIdentity( + privateKey: webcrypto.CryptoKey, + publicKeyBytes: Uint8Array, + alg: number, +): Promise { + const identityKey: IdentityKey = { + alg, + "public-key": toBufferSource(publicKeyBytes), + }; + const deviceId = await deriveDeviceId(publicKeyBytes); + + return { + deviceId, + identityKey, + async sign(message) { + const signature = await webcrypto.subtle.sign( + algParams(alg), + privateKey, + toBufferSource(message), + ); + return new Uint8Array(signature); + }, + verify: verifyWithPublicKey, + deriveDeviceId, + }; +} diff --git a/ts/packages/core/src/adapters/system-clock.ts b/ts/packages/core/src/adapters/system-clock.ts new file mode 100644 index 0000000..a3dbbd7 --- /dev/null +++ b/ts/packages/core/src/adapters/system-clock.ts @@ -0,0 +1,8 @@ +import type { Clock } from "../ports/clock.js"; + +/** Wraps the system wall clock -- the only place Date.now() appears in this package. */ +export function createSystemClock(): Clock { + return { + now: () => Date.now(), + }; +} diff --git a/ts/packages/core/src/adapters/tcp-transport.ts b/ts/packages/core/src/adapters/tcp-transport.ts new file mode 100644 index 0000000..b2e16c5 --- /dev/null +++ b/ts/packages/core/src/adapters/tcp-transport.ts @@ -0,0 +1,143 @@ +import { connect as netConnect, createServer, type Socket } from "node:net"; +import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2"; +import { frameSchema, type Frame } from "../generated/protocol.js"; +import type { Connection, Transport } from "../ports/transport.js"; + +const LENGTH_PREFIX_BYTES = 4; + +function parseAddress(address: string): { host: string; port: number } { + const lastColon = address.lastIndexOf(":"); + if (lastColon === -1) { + throw new Error(`expected "host:port", got "${address}"`); + } + return { + host: address.slice(0, lastColon), + port: Number(address.slice(lastColon + 1)), + }; +} + +function writeFrame(socket: Socket, frame: Frame): void { + const body = encode(frame, cdeEncodeOptions); + const header = Buffer.alloc(LENGTH_PREFIX_BYTES); + header.writeUInt32BE(body.length, 0); + socket.write(header); + socket.write(body); +} + +/** Reassembles length-prefixed CBOR frames from a byte stream, validating each against frameSchema before handing it to a consumer. */ +function frameReader(socket: Socket): AsyncIterable { + let buffer = Buffer.alloc(0); + const pending: Frame[] = []; + const waiters: ((value: IteratorResult) => void)[] = []; + let ended = false; + + function tryDrain(): void { + while (buffer.length >= LENGTH_PREFIX_BYTES) { + const bodyLength = buffer.readUInt32BE(0); + if (buffer.length < LENGTH_PREFIX_BYTES + bodyLength) break; + const body = buffer.subarray( + LENGTH_PREFIX_BYTES, + LENGTH_PREFIX_BYTES + bodyLength, + ); + buffer = buffer.subarray(LENGTH_PREFIX_BYTES + bodyLength); + + const decoded: unknown = decode(body, cdeDecodeOptions); + const result = frameSchema.safeParse(decoded); + if (result.success) { + const waiter = waiters.shift(); + if (waiter) { + waiter({ value: result.data, done: false }); + } else { + pending.push(result.data); + } + } + // A frame that fails schema validation is silently dropped from the stream rather than closing the connection -- an unrecognised frame from a newer peer is exactly what version negotiation exists to tolerate. + } + } + + function endAll(): void { + ended = true; + for (const waiter of waiters.splice(0)) { + waiter({ value: undefined, done: true }); + } + } + + socket.on("data", (chunk: Buffer) => { + buffer = Buffer.concat([buffer, chunk]); + tryDrain(); + }); + socket.on("end", endAll); + socket.on("close", endAll); + + return { + [Symbol.asyncIterator]() { + return { + async next(): Promise> { + const next = pending.shift(); + if (next !== undefined) { + return Promise.resolve({ value: next, done: false }); + } + if (ended) { + return Promise.resolve({ value: undefined, done: true }); + } + return new Promise((resolve) => { + waiters.push(resolve); + }); + }, + }; + }, + }; +} + +function wrapSocket(socket: Socket): Connection { + const frames = frameReader(socket); + return { + // Socket.write is not itself async; the contract stays Promise-returning so other adapters (e.g. one with real backpressure/ack semantics) can be genuinely asynchronous. + send: async (frame) => { + writeFrame(socket, frame); + return Promise.resolve(); + }, + receive: () => frames, + close: async () => + new Promise((resolve) => { + socket.end(() => { + resolve(); + }); + }), + }; +} + +/** A Node net.Socket-based Transport: length-prefixed, CBOR-encoded frames over plain TCP -- matching Cascade's own transport shape, since interop with Cascade nodes is wire-mesh's stated goal. Framing (not TLS) is this adapter's own concern; a TLS-terminated variant is a separate adapter behind the same Transport contract. */ +export function createTcpTransport(): Transport { + return { + async connect(address) { + const { host, port } = parseAddress(address); + return new Promise((resolve, reject) => { + const socket = netConnect({ host, port }); + socket.once("connect", () => { + resolve(wrapSocket(socket)); + }); + socket.once("error", reject); + }); + }, + async listen(address, onConnection) { + const { host, port } = parseAddress(address); + return new Promise((resolve, reject) => { + const server = createServer((socket) => { + onConnection(wrapSocket(socket)); + }); + server.once("error", reject); + server.listen(port, host, () => { + resolve( + async () => + new Promise((resolveClose) => { + server.close(() => { + resolveClose(); + }); + }), + ); + }); + }); + }, + }; +} diff --git a/ts/packages/core/src/domain/handshake.ts b/ts/packages/core/src/domain/handshake.ts new file mode 100644 index 0000000..28e5793 --- /dev/null +++ b/ts/packages/core/src/domain/handshake.ts @@ -0,0 +1,34 @@ +import type { + DomainId, + HandshakeFrame, + ProtocolVersion, +} from "../generated/protocol.js"; + +/** The highest protocol version this build of core understands. */ +export const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion = 1; + +export interface NegotiationResult { + ok: boolean; + /** The version both peers will speak for the rest of the session -- the lower of the two offered versions, so a peer never has to understand a frame shape it didn't advertise. */ + version: ProtocolVersion; + /** Domains both peers advertised -- the only ones either side may address for the rest of the session. */ + sharedDomains: DomainId[]; +} + +/** + * Negotiates protocol version and capability domains between a local and remote handshake -- the mechanism wire-mesh's handshake exists to provide, and agent-comms issue #31's fix: a mixed fleet of old and new peers negotiates down to what they both actually support, rather than one side silently misinterpreting frames the other can't produce yet. + */ +export function negotiate( + local: HandshakeFrame, + remote: HandshakeFrame, +): NegotiationResult { + const version = Math.min(local.version, remote.version); + const sharedDomains = local.domains.filter((domain) => + remote.domains.includes(domain), + ); + return { + ok: version >= 1 && sharedDomains.length > 0, + version, + sharedDomains, + }; +} diff --git a/ts/packages/core/src/domain/tokens.ts b/ts/packages/core/src/domain/tokens.ts new file mode 100644 index 0000000..2b644c1 --- /dev/null +++ b/ts/packages/core/src/domain/tokens.ts @@ -0,0 +1,133 @@ +import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2"; +import { + capabilityTokenSchema, + tokenClaimsSchema, + type CapabilityToken, + type DeviceId, + type TokenClaims, +} from "../generated/protocol.js"; +import type { Clock } from "../ports/clock.js"; +import type { IdentityPort } from "../ports/identity.js"; + +export interface RevocationCheck { + isRevoked: (tokenId: Uint8Array) => Promise; +} + +export type TokenVerdictReason = + | "malformed" + | "bad_signature" + | "wrong_issuer" + | "bearer_mismatch" + | "expired" + | "not_yet_valid" + | "revoked" + | "delegation_exceeds_parent" + | "parent_invalid"; + +export type TokenVerdict = + { ok: true; claims: TokenClaims } | { ok: false; reason: TokenVerdictReason }; + +export interface VerifyCapabilityTokenOptions { + identity: IdentityPort; + clock: Clock; + revocation: RevocationCheck; + /** When given, the token must bear this device -- the caller presenting a token to authorise itself, not someone else. */ + expectedBearer?: DeviceId; +} + +function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i += 1) { + if (a[i] !== b[i]) return false; + } + return true; +} + +/** RFC 9052 §4.4 Sig_structure for a COSE_Sign1 with no external AAD: ["Signature1", protected, external_aad, payload]. */ +function sig1ToBeSigned( + protectedHeader: Uint8Array, + payload: Uint8Array, +): Uint8Array { + return encode( + ["Signature1", protectedHeader, new Uint8Array(0), payload], + cdeEncodeOptions, + ); +} + +/** + * Verifies one capability token per tokens.cddl's own documented rules: the token is a well-formed COSE_Sign1 whose signature actually verifies against its own embedded issuer-key, that issuer-key is self-certifying (sha256(issuer-key.public-key) equals the claimed issuer device-id -- no shared secret needed to check this), the token is currently valid (not expired, not before not-before, not revoked), and -- recursively -- any parent delegation narrows rather than widens: the parent's bearer must be this token's issuer (the delegation chain is unbroken), and this token's expiry must not exceed its parent's. + */ +export async function verifyCapabilityToken( + token: CapabilityToken, + options: VerifyCapabilityTokenOptions, +): Promise { + const [protectedHeader, , payload, signature] = token; + if (payload === null) { + return { ok: false, reason: "malformed" }; + } + + const decodedClaims: unknown = decode(payload, cdeDecodeOptions); + const claimsResult = tokenClaimsSchema.safeParse(decodedClaims); + if (!claimsResult.success) { + return { ok: false, reason: "malformed" }; + } + const claims = claimsResult.data; + + const signatureOk = await options.identity.verify( + claims["issuer-key"], + sig1ToBeSigned(protectedHeader, payload), + signature, + ); + if (!signatureOk) { + return { ok: false, reason: "bad_signature" }; + } + + const derivedIssuerId = await options.identity.deriveDeviceId( + claims["issuer-key"]["public-key"], + ); + if (!bytesEqual(derivedIssuerId, claims.issuer)) { + return { ok: false, reason: "wrong_issuer" }; + } + + const now = options.clock.now(); + if (claims.expires <= now) { + return { ok: false, reason: "expired" }; + } + if (claims["not-before"] !== undefined && claims["not-before"] > now) { + return { ok: false, reason: "not_yet_valid" }; + } + + if (await options.revocation.isRevoked(claims["token-id"])) { + return { ok: false, reason: "revoked" }; + } + + if (claims.parent !== undefined) { + const decodedParent: unknown = decode(claims.parent, cdeDecodeOptions); + const parentResult = capabilityTokenSchema.safeParse(decodedParent); + if (!parentResult.success) { + return { ok: false, reason: "parent_invalid" }; + } + const parentVerdict = await verifyCapabilityToken( + parentResult.data, + options, + ); + if (!parentVerdict.ok) { + return { ok: false, reason: "parent_invalid" }; + } + if (!bytesEqual(parentVerdict.claims.bearer, claims.issuer)) { + return { ok: false, reason: "delegation_exceeds_parent" }; + } + if (claims.expires > parentVerdict.claims.expires) { + return { ok: false, reason: "delegation_exceeds_parent" }; + } + } + + if ( + options.expectedBearer !== undefined && + !bytesEqual(claims.bearer, options.expectedBearer) + ) { + return { ok: false, reason: "bearer_mismatch" }; + } + + return { ok: true, claims }; +} diff --git a/ts/packages/core/src/generated/protocol.ts b/ts/packages/core/src/generated/protocol.ts new file mode 100644 index 0000000..807b176 --- /dev/null +++ b/ts/packages/core/src/generated/protocol.ts @@ -0,0 +1,350 @@ +// Generated by cddl.js. Do not edit by hand -- regenerate from the source .cddl instead. + +import { z } from "zod"; + +export const dataHaveFrameSchema = z.lazy(() => z.object({ + "type": z.literal("data-have"), + "peer": z.lazy(() => deviceIdSchema), + "head-seq": z.number().int().nonnegative(), +})); +export const dataRequestFrameSchema = z.lazy(() => z.object({ + "type": z.literal("data-request"), + "peer": z.lazy(() => deviceIdSchema), + "from-seq": z.number().int().nonnegative(), +})); +export const dataEntriesFrameSchema = z.lazy(() => z.object({ + "type": z.literal("data-entries"), + "peer": z.lazy(() => deviceIdSchema), + "from-seq": z.number().int().nonnegative(), + "entries": z.array(z.instanceof(Uint8Array)), +})); +export const handleClaimsSchema = z.lazy(() => z.object({ + "handle": z.string(), + "device-id": z.lazy(() => deviceIdSchema), + "identity-key": z.lazy(() => identityKeySchema), + "candidates": z.array(z.lazy(() => wireCandidateSchema)).optional(), + "mailbox": z.lazy(() => deviceIdSchema).optional(), + "issued": z.number().int().nonnegative(), + "expires": z.number().int().nonnegative(), +})); +export const handleRecordSchema = z.lazy(() => z.lazy(() => coseSign1Schema)); +export const manageCommandParamsSchema = z.lazy(() => z.union([z.union([z.lazy(() => ptySpawnSchema), z.lazy(() => ptyWriteSchema), z.lazy(() => ptyResizeSchema), z.lazy(() => ptyKillSchema), z.lazy(() => procSpawnSchema), z.lazy(() => procSignalSchema), z.lazy(() => procKillSchema), z.lazy(() => execListSchema)]), z.object({ + +}).catchall(z.unknown())])); +export const ptySpawnSchema = z.lazy(() => z.object({ + "verb": z.literal("pty.spawn"), + "shell": z.string().optional(), + "argv": z.array(z.string()), + "cwd": z.string().optional(), + "env": z.object({ + +}).catchall(z.string()), + "cols": z.number().int().nonnegative(), + "rows": z.number().int().nonnegative(), +})); +export const ptyWriteSchema = z.lazy(() => z.object({ + "verb": z.literal("pty.write"), + "session": z.lazy(() => streamSessionSchema), + "bytes": z.instanceof(Uint8Array), +})); +export const ptyResizeSchema = z.lazy(() => z.object({ + "verb": z.literal("pty.resize"), + "session": z.lazy(() => streamSessionSchema), + "cols": z.number().int().nonnegative(), + "rows": z.number().int().nonnegative(), +})); +export const ptyKillSchema = z.lazy(() => z.object({ + "verb": z.literal("pty.kill"), + "session": z.lazy(() => streamSessionSchema), + "signal": z.number().int(), +})); +export const procSpawnSchema = z.lazy(() => z.object({ + "verb": z.literal("proc.spawn"), + "argv": z.array(z.string()), + "cwd": z.string().optional(), + "env": z.object({ + +}).catchall(z.string()), +})); +export const procSignalSchema = z.lazy(() => z.object({ + "verb": z.literal("proc.signal"), + "session": z.lazy(() => streamSessionSchema), + "signal": z.number().int(), +})); +export const procKillSchema = z.lazy(() => z.object({ + "verb": z.literal("proc.kill"), + "session": z.lazy(() => streamSessionSchema), +})); +export const execListSchema = z.lazy(() => z.object({ + "verb": z.literal("exec.list"), +})); +export const execSessionInfoSchema = z.lazy(() => z.object({ + "session": z.lazy(() => streamSessionSchema), + "kind": z.union([z.literal("pty"), z.literal("proc")]), + "argv": z.array(z.string()).optional(), + "cwd": z.string().optional(), +})); +export const meshIdSchema = z.lazy(() => z.string()); +export const federationLinkRequestFrameSchema = z.lazy(() => z.object({ + "type": z.literal("federation-link-request"), + "local-mesh": z.lazy(() => meshIdSchema), + "local-name": z.string(), + "offered-shares": z.array(z.lazy(() => shareDescriptorSchema)), +})); +export const federationLinkAcceptFrameSchema = z.lazy(() => z.object({ + "type": z.literal("federation-link-accept"), + "remote-mesh": z.lazy(() => meshIdSchema), + "remote-name": z.string(), + "accepted-shares": z.array(z.lazy(() => shareDescriptorSchema)), +})); +export const federationLinkRejectFrameSchema = z.lazy(() => z.object({ + "type": z.literal("federation-link-reject"), + "reason": z.string(), +})); +export const shareDescriptorSchema = z.lazy(() => z.object({ + "domain": z.lazy(() => domainIdSchema), + "resource": z.lazy(() => capabilityScopeSchema), + "direction": z.union([z.literal("inbound"), z.literal("outbound"), z.literal("bidirectional")]), +})); +export const federationShareFrameSchema = z.lazy(() => z.object({ + "type": z.literal("federation-share"), + "share": z.lazy(() => shareDescriptorSchema), +})); +export const federationUnshareFrameSchema = z.lazy(() => z.object({ + "type": z.literal("federation-unshare"), + "share": z.lazy(() => shareDescriptorSchema), +})); +export const federationEnvelopeFrameSchema = z.lazy(() => z.object({ + "type": z.literal("federation-envelope"), + "origin-mesh": z.lazy(() => meshIdSchema), + "origin-device": z.lazy(() => deviceIdSchema), + "resource": z.lazy(() => capabilityScopeSchema), + "inner": z.instanceof(Uint8Array), +})); +export const frameVariantSchema = z.lazy(() => z.union([z.lazy(() => handshakeFrameSchema), z.lazy(() => pingFrameSchema), z.lazy(() => closeFrameSchema), z.lazy(() => gossipFrameSchema), z.lazy(() => candidatesFrameSchema), z.lazy(() => syncPunchFrameSchema), z.lazy(() => observedAddressFrameSchema), z.lazy(() => relayOfferFrameSchema), z.lazy(() => relayConnectFrameSchema), z.lazy(() => relayDataFrameSchema), z.lazy(() => relayInboundFrameSchema), z.lazy(() => manageRequestFrameSchema), z.lazy(() => manageResponseFrameSchema), z.lazy(() => revocationAnnounceFrameSchema), z.lazy(() => streamDataFrameSchema), z.lazy(() => streamAckFrameSchema), z.lazy(() => streamEndFrameSchema), z.lazy(() => dataHaveFrameSchema), z.lazy(() => dataRequestFrameSchema), z.lazy(() => dataEntriesFrameSchema), z.lazy(() => federationLinkRequestFrameSchema), z.lazy(() => federationLinkAcceptFrameSchema), z.lazy(() => federationLinkRejectFrameSchema), z.lazy(() => federationShareFrameSchema), z.lazy(() => federationUnshareFrameSchema), z.lazy(() => federationEnvelopeFrameSchema)])); +export const frameSchema = z.lazy(() => z.lazy(() => frameVariantSchema)); +export const protocolVersionSchema = z.lazy(() => z.number().int().nonnegative()); +export const domainIdSchema = z.lazy(() => z.union([z.lazy(() => coreDomainNameSchema), z.lazy(() => namespacedDomainIdSchema), z.lazy(() => privateUseDomainIdSchema)])); +export const coreDomainNameSchema = z.lazy(() => z.union([z.literal("core/management"), z.literal("core/exec"), z.literal("core/data"), z.literal("core/federation")])); +export const namespacedDomainIdSchema = z.lazy(() => z.string().regex(new RegExp("[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+/[A-Za-z0-9_.-]+"))); +export const privateUseDomainIdSchema = z.lazy(() => z.string().regex(new RegExp("x-[A-Za-z0-9_.-]+"))); +export const handshakeFrameSchema = z.lazy(() => z.object({ + "type": z.literal("handshake"), + "version": z.lazy(() => protocolVersionSchema), + "domains": z.array(z.lazy(() => domainIdSchema)), + "params": z.object({ + +}).catchall(z.unknown()).optional(), +})); +export const identityKeySchema = z.lazy(() => z.object({ + "alg": z.number().int(), + "public-key": z.instanceof(Uint8Array), +})); +export const deviceIdSchema = z.lazy(() => z.instanceof(Uint8Array).refine((v) => v.length === 32, { message: "expected exactly 32 bytes" })); +export const peerIdentitySchema = z.lazy(() => z.object({ + "device-id": z.lazy(() => deviceIdSchema), + "identity-key": z.lazy(() => identityKeySchema), + "certificate": z.instanceof(Uint8Array).optional(), +})); +export const manageCommandSchema = z.lazy(() => z.object({ + "verb": z.lazy(() => capabilityVerbSchema), + "params": z.lazy(() => manageCommandParamsSchema), +})); +export const manageRequestFrameSchema = z.lazy(() => z.object({ + "type": z.literal("manage-request"), + "request-id": z.number().int().nonnegative(), + "command": z.lazy(() => manageCommandSchema), + "scope": z.lazy(() => capabilityScopeSchema), + "token": z.lazy(() => capabilityTokenSchema).optional(), +})); +export const manageOkSchema = z.lazy(() => z.object({ + "result": z.literal("ok"), +}).catchall(z.unknown())); +export const manageErrorSchema = z.lazy(() => z.object({ + "result": z.literal("error"), + "code": z.string(), + "message": z.string().optional(), +})); +export const manageResponseFrameSchema = z.lazy(() => z.object({ + "type": z.literal("manage-response"), + "request-id": z.number().int().nonnegative(), + "outcome": z.union([z.lazy(() => manageOkSchema), z.lazy(() => manageErrorSchema)]), +})); +export const revocationEntrySchema = z.lazy(() => z.object({ + "token-id": z.instanceof(Uint8Array), + "revoked-at": z.number().int().nonnegative(), +})); +export const revocationAnnounceFrameSchema = z.lazy(() => z.object({ + "type": z.literal("revocation-announce"), + "entries": z.array(z.lazy(() => revocationEntrySchema)), +})); +export const streamSessionSchema = z.lazy(() => z.number().int().nonnegative()); +export const streamDataFrameSchema = z.lazy(() => z.object({ + "type": z.literal("stream-data"), + "session": z.lazy(() => streamSessionSchema), + "seq": z.number().int().nonnegative(), + "channel": z.string(), + "bytes": z.instanceof(Uint8Array), +})); +export const streamAckFrameSchema = z.lazy(() => z.object({ + "type": z.literal("stream-ack"), + "session": z.lazy(() => streamSessionSchema), + "ack-seq": z.number().int().nonnegative(), + "window": z.number().int().nonnegative(), +})); +export const streamEndFrameSchema = z.lazy(() => z.object({ + "type": z.literal("stream-end"), + "session": z.lazy(() => streamSessionSchema), + "exit-code": z.number().int().optional(), + "exit-signal": z.number().int().optional(), +})); +export const capabilityVerbSchema = z.lazy(() => z.union([z.lazy(() => coreCapabilitySchema), z.lazy(() => namespacedCapabilitySchema), z.lazy(() => privateUseCapabilitySchema)])); +export const coreCapabilitySchema = z.lazy(() => z.string().regex(new RegExp("[a-z][a-z0-9-]*:[a-z][a-z0-9-]*"))); +export const namespacedCapabilitySchema = z.lazy(() => z.string().regex(new RegExp("[a-z0-9.-]+/[A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+"))); +export const privateUseCapabilitySchema = z.lazy(() => z.string().regex(new RegExp("x-[A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+"))); +export const capabilityScopeSchema = z.lazy(() => z.object({ + "kind": z.string(), + "path": z.string().optional(), +})); +export const coseHeaderAlgSchema = z.lazy(() => z.literal(1)); +export const coseHeaderKidSchema = z.lazy(() => z.literal(4)); +export const coseHeaderLabelSchema = z.lazy(() => z.union([z.number().int(), z.string()])); +export const coseTokenHeadersSchema = z.lazy(() => z.object({ + "1": z.number().int().optional(), + "4": z.instanceof(Uint8Array).optional(), +}).catchall(z.unknown())); +export const coseSign1Schema = z.lazy(() => z.tuple([z.instanceof(Uint8Array), z.lazy(() => coseTokenHeadersSchema), z.union([z.instanceof(Uint8Array), z.null()]), z.instanceof(Uint8Array)])); +export const capabilityTokenSchema = z.lazy(() => z.lazy(() => coseSign1Schema)); +export const tokenClaimsSchema = z.lazy(() => z.object({ + "token-id": z.instanceof(Uint8Array), + "issuer": z.lazy(() => deviceIdSchema), + "issuer-key": z.lazy(() => identityKeySchema), + "bearer": z.lazy(() => deviceIdSchema), + "capability": z.lazy(() => capabilityVerbSchema), + "scope": z.lazy(() => capabilityScopeSchema), + "expires": z.number().int().nonnegative(), + "not-before": z.number().int().nonnegative().optional(), + "parent": z.instanceof(Uint8Array).optional(), +}).catchall(z.unknown())); +export const pingFrameSchema = z.lazy(() => z.object({ + "type": z.literal("ping"), +})); +export const closeFrameSchema = z.lazy(() => z.object({ + "type": z.literal("close"), + "reason": z.string().optional(), +})); +export const peerAdvertSchema = z.lazy(() => z.object({ + "device": z.lazy(() => deviceIdSchema), + "addresses": z.array(z.string()), + "snapshot-seconds": z.number().int(), +})); +export const gossipFrameSchema = z.lazy(() => z.object({ + "type": z.literal("gossip"), + "peers": z.array(z.lazy(() => peerAdvertSchema)), +})); +export const candidateKindSchema = z.lazy(() => z.union([z.literal("host"), z.literal("server-reflexive"), z.literal("relayed")])); +export const wireCandidateSchema = z.lazy(() => z.object({ + "address": z.string(), + "kind": z.lazy(() => candidateKindSchema), + "priority": z.number().int().nonnegative(), +})); +export const candidatesFrameSchema = z.lazy(() => z.object({ + "type": z.literal("candidates"), + "candidates": z.array(z.lazy(() => wireCandidateSchema)), +})); +export const syncPunchFrameSchema = z.lazy(() => z.object({ + "type": z.literal("sync-punch"), + "nonce": z.number().int().nonnegative(), + "deadline-unix-ms": z.number().int().nonnegative(), +})); +export const observedAddressFrameSchema = z.lazy(() => z.object({ + "type": z.literal("observed-address"), + "address": z.string(), +})); +export const relayOfferFrameSchema = z.lazy(() => z.object({ + "type": z.literal("relay-offer"), + "addresses": z.array(z.string()), +})); +export const relayConnectFrameSchema = z.lazy(() => z.object({ + "type": z.literal("relay-connect"), + "target-device": z.lazy(() => deviceIdSchema), +})); +export const relayDataFrameSchema = z.lazy(() => z.object({ + "type": z.literal("relay-data"), + "payload": z.instanceof(Uint8Array), +})); +export const relayInboundFrameSchema = z.lazy(() => z.object({ + "type": z.literal("relay-inbound"), + "source-device": z.lazy(() => deviceIdSchema), +})); + +export type DataHaveFrame = z.infer; +export type DataRequestFrame = z.infer; +export type DataEntriesFrame = z.infer; +export type HandleClaims = z.infer; +export type HandleRecord = z.infer; +export type ManageCommandParams = z.infer; +export type PtySpawn = z.infer; +export type PtyWrite = z.infer; +export type PtyResize = z.infer; +export type PtyKill = z.infer; +export type ProcSpawn = z.infer; +export type ProcSignal = z.infer; +export type ProcKill = z.infer; +export type ExecList = z.infer; +export type ExecSessionInfo = z.infer; +export type MeshId = z.infer; +export type FederationLinkRequestFrame = z.infer; +export type FederationLinkAcceptFrame = z.infer; +export type FederationLinkRejectFrame = z.infer; +export type ShareDescriptor = z.infer; +export type FederationShareFrame = z.infer; +export type FederationUnshareFrame = z.infer; +export type FederationEnvelopeFrame = z.infer; +export type FrameVariant = z.infer; +export type Frame = z.infer; +export type ProtocolVersion = z.infer; +export type DomainId = z.infer; +export type CoreDomainName = z.infer; +export type NamespacedDomainId = z.infer; +export type PrivateUseDomainId = z.infer; +export type HandshakeFrame = z.infer; +export type IdentityKey = z.infer; +export type DeviceId = z.infer; +export type PeerIdentity = z.infer; +export type ManageCommand = z.infer; +export type ManageRequestFrame = z.infer; +export type ManageOk = z.infer; +export type ManageError = z.infer; +export type ManageResponseFrame = z.infer; +export type RevocationEntry = z.infer; +export type RevocationAnnounceFrame = z.infer; +export type StreamSession = z.infer; +export type StreamDataFrame = z.infer; +export type StreamAckFrame = z.infer; +export type StreamEndFrame = z.infer; +export type CapabilityVerb = z.infer; +export type CoreCapability = z.infer; +export type NamespacedCapability = z.infer; +export type PrivateUseCapability = z.infer; +export type CapabilityScope = z.infer; +export type CoseHeaderAlg = z.infer; +export type CoseHeaderKid = z.infer; +export type CoseHeaderLabel = z.infer; +export type CoseTokenHeaders = z.infer; +export type CoseSign1 = z.infer; +export type CapabilityToken = z.infer; +export type TokenClaims = z.infer; +export type PingFrame = z.infer; +export type CloseFrame = z.infer; +export type PeerAdvert = z.infer; +export type GossipFrame = z.infer; +export type CandidateKind = z.infer; +export type WireCandidate = z.infer; +export type CandidatesFrame = z.infer; +export type SyncPunchFrame = z.infer; +export type ObservedAddressFrame = z.infer; +export type RelayOfferFrame = z.infer; +export type RelayConnectFrame = z.infer; +export type RelayDataFrame = z.infer; +export type RelayInboundFrame = z.infer; diff --git a/ts/packages/core/src/generated/runtime.ts b/ts/packages/core/src/generated/runtime.ts new file mode 100644 index 0000000..ba4b266 --- /dev/null +++ b/ts/packages/core/src/generated/runtime.ts @@ -0,0 +1,2 @@ +// The generated protocol.ts module (produced by generate.ts, committed alongside this file) imports its cbor-decoding helper as a same-directory sibling, "./runtime.js" -- matching where cddl.js's own CLI writes output. This re-export gives it that sibling here, without duplicating cddl.js's own runtime.ts content. +export { cborDecodesAs } from "cddl.js/runtime"; diff --git a/ts/packages/core/src/ports/clock.ts b/ts/packages/core/src/ports/clock.ts new file mode 100644 index 0000000..5510a8c --- /dev/null +++ b/ts/packages/core/src/ports/clock.ts @@ -0,0 +1,5 @@ +// Injected time source, never pulled from global Date.now() directly in domain logic -- lets token-expiry and revocation-timestamp checks be tested deterministically and lets a consumer substitute a synchronised network clock later without touching domain code. +export interface Clock { + /** Current time as Unix milliseconds, matching every uint timestamp field in the protocol (token-claims.expires, revocation-entry.revoked-at, handshake params, etc.). */ + now: () => number; +} diff --git a/ts/packages/core/src/ports/identity.ts b/ts/packages/core/src/ports/identity.ts new file mode 100644 index 0000000..8df3d74 --- /dev/null +++ b/ts/packages/core/src/ports/identity.ts @@ -0,0 +1,19 @@ +import type { DeviceId, IdentityKey } from "../generated/protocol.js"; + +/** + * Wraps the local node's own identity plus the signing/verification primitives capability-token handling needs -- never hardcoded to one crypto library's own API shape (Node's webcrypto, a WASM implementation, a hardware key, etc. can all satisfy this same contract). + */ +export interface IdentityPort { + readonly deviceId: DeviceId; + readonly identityKey: IdentityKey; + /** Signs an already-assembled message (e.g. a COSE Sig_structure) with the local node's own private key. Always a fresh buffer, never a view into shared or offset memory -- directly usable as a CapabilityToken tuple element without further normalisation. */ + sign: (message: Uint8Array) => Promise>; + /** Verifies a signature against an arbitrary identity-key -- not necessarily the local node's own, since capability-token verification checks a token's issuer-key. */ + verify: ( + key: IdentityKey, + message: Uint8Array, + signature: Uint8Array, + ) => Promise; + /** Derives the device-id an arbitrary public key would produce (SHA-256 of the raw public-key bytes -- never a certificate's own DER encoding, the bug both Cascade and agent-comms had to fix). Used to check a token's self-certifying issuer-key against its claimed issuer, not just the local node's own identity. */ + deriveDeviceId: (publicKey: Uint8Array) => Promise; +} diff --git a/ts/packages/core/src/ports/storage.ts b/ts/packages/core/src/ports/storage.ts new file mode 100644 index 0000000..d2790d1 --- /dev/null +++ b/ts/packages/core/src/ports/storage.ts @@ -0,0 +1,8 @@ +// A minimal async key/value contract for whatever this package needs to persist (revocation entries, data-domain oplog entries) -- local and remote implementations interchangeable behind it, per the org's storage-boundary rules. Values are opaque bytes, not a specific serialisation: callers encode/decode with whatever schema the stored data actually has (a revocation-entry, a data-domain entry, etc.), matching the protocol's own treatment of data-domain entries as opaque to the wire format. +export interface KeyValueStorage { + get: (key: string) => Promise; + set: (key: string, value: Uint8Array) => Promise; + delete: (key: string) => Promise; + /** Keys with the given prefix, in no particular order. */ + keys: (prefix: string) => Promise; +} diff --git a/ts/packages/core/src/ports/transport.ts b/ts/packages/core/src/ports/transport.ts new file mode 100644 index 0000000..94d3c69 --- /dev/null +++ b/ts/packages/core/src/ports/transport.ts @@ -0,0 +1,20 @@ +import type { Frame } from "../generated/protocol.js"; + +/** + * An async, serialisable-data contract for exchanging frame values over some connection -- deliberately without baking in any specific transport's own primitives (no raw socket types, no WebSocket-specific options), so a TCP adapter, a WebSocket adapter, or an in-memory adapter for tests can all satisfy it. + */ +export interface Connection { + send: (frame: Frame) => Promise; + /** Frames received on this connection, in arrival order, until the connection closes. */ + receive: () => AsyncIterable; + close: () => Promise; +} + +export interface Transport { + connect: (address: string) => Promise; + /** Starts listening; each accepted connection is handed to onConnection. Returns a function that stops listening and closes the listener. */ + listen: ( + address: string, + onConnection: (connection: Readonly) => void, + ) => Promise<() => Promise>; +} diff --git a/ts/packages/core/test/conformance.test.ts b/ts/packages/core/test/conformance.test.ts new file mode 100644 index 0000000..bda46f6 --- /dev/null +++ b/ts/packages/core/test/conformance.test.ts @@ -0,0 +1,85 @@ +// This package's own conformance-check: every vector in conformance/'s golden files decodes to something the generated schemas accept, and re-encoding the validated value reproduces the recorded wire_hex exactly. Mirrors cddl.js's own round-trip test, but here the schemas come from this package's real dependency on cddl.js plus its committed src/generated/protocol.ts, not a local emitModule() call. + +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2"; +import { + capabilityTokenSchema, + frameSchema, +} from "../src/generated/protocol.js"; + +interface Vector { + name: string; + wire_hex: string; +} + +interface VectorFile { + vectors: Vector[]; +} + +function isVectorFile(value: unknown): value is VectorFile { + if (typeof value !== "object" || value === null) return false; + if (!("vectors" in value) || !Array.isArray(value.vectors)) return false; + return value.vectors.every( + (v: unknown) => + typeof v === "object" && + v !== null && + "name" in v && + "wire_hex" in v && + typeof v.name === "string" && + typeof v.wire_hex === "string", + ); +} + +function readVectors(filename: string): Vector[] { + const path = fileURLToPath( + new URL(`../../../../conformance/${filename}`, import.meta.url), + ); + const raw: unknown = JSON.parse(readFileSync(path, "utf8")); + if (!isVectorFile(raw)) { + throw new Error(`${filename} is not a valid vector file`); + } + return raw.vectors; +} + +function roundTrip( + vector: Readonly, + schema: Readonly<{ + safeParse: (value: unknown) => { success: boolean; data?: unknown }; + }>, +): void { + // A plain Uint8Array, not a Node Buffer -- cbor2 slices embedded byte strings via the input's own subarray(), which on a Buffer returns another Buffer, a constructor cbor2's own encoder doesn't recognise as a byte string. + const bytes = Uint8Array.from(Buffer.from(vector.wire_hex, "hex")); + const decoded: unknown = decode(bytes, cdeDecodeOptions); + + const result = schema.safeParse(decoded); + expect(result.success, `schema rejected vector "${vector.name}"`).toBe(true); + + const reEncoded = Buffer.from(encode(result.data, cdeEncodeOptions)).toString( + "hex", + ); + expect( + reEncoded, + `re-encoding "${vector.name}" did not reproduce wire_hex`, + ).toBe(vector.wire_hex); +} + +describe("frame vectors decode and re-encode byte-exactly through frameSchema", () => { + for (const vector of [ + ...readVectors("frames.v1.json"), + ...readVectors("handshake.v1.json"), + ]) { + it(vector.name, () => { + roundTrip(vector, frameSchema); + }); + } +}); + +describe("token vectors decode and re-encode byte-exactly through capabilityTokenSchema", () => { + for (const vector of readVectors("tokens.v1.json")) { + it(vector.name, () => { + roundTrip(vector, capabilityTokenSchema); + }); + } +}); diff --git a/ts/packages/core/test/handshake.test.ts b/ts/packages/core/test/handshake.test.ts new file mode 100644 index 0000000..d46ae74 --- /dev/null +++ b/ts/packages/core/test/handshake.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import { negotiate } from "../src/domain/handshake.js"; +import type { HandshakeFrame } from "../src/generated/protocol.js"; + +function handshake( + version: number, + domains: readonly string[], +): HandshakeFrame { + return { type: "handshake", version, domains: [...domains] }; +} + +describe("negotiate", () => { + it("negotiates the lower of the two offered versions", () => { + const result = negotiate( + handshake(2, ["core/management"]), + handshake(1, ["core/management"]), + ); + expect(result.version).toBe(1); + }); + + it("negotiates the shared domains, in local order", () => { + const result = negotiate( + handshake(1, ["core/management", "core/exec", "core/data"]), + handshake(1, ["core/data", "core/management"]), + ); + expect(result.sharedDomains).toEqual(["core/management", "core/data"]); + }); + + it("fails when the two peers share no domains", () => { + const result = negotiate( + handshake(1, ["core/exec"]), + handshake(1, ["core/data"]), + ); + expect(result.ok).toBe(false); + expect(result.sharedDomains).toEqual([]); + }); + + it("succeeds when versions and domains both overlap", () => { + const result = negotiate( + handshake(1, ["core/management"]), + handshake(1, ["core/management"]), + ); + expect(result.ok).toBe(true); + }); +}); diff --git a/ts/packages/core/test/tokens.test.ts b/ts/packages/core/test/tokens.test.ts new file mode 100644 index 0000000..f652824 --- /dev/null +++ b/ts/packages/core/test/tokens.test.ts @@ -0,0 +1,320 @@ +import { webcrypto } from "node:crypto"; +import { beforeAll, describe, expect, it } from "vitest"; +import { cdeEncodeOptions, encode } from "cbor2"; +import { createNodeIdentity } from "../src/adapters/node-identity.js"; +import { createMemoryStorage } from "../src/adapters/memory-storage.js"; +import { createSystemClock } from "../src/adapters/system-clock.js"; +import { + verifyCapabilityToken, + type RevocationCheck, +} from "../src/domain/tokens.js"; +import type { IdentityPort } from "../src/ports/identity.js"; +import type { Clock } from "../src/ports/clock.js"; +import type { + CapabilityScope, + CapabilityToken, + DeviceId, + TokenClaims, +} from "../src/generated/protocol.js"; + +const ES256 = -7; +const HOUR_MS = 3_600_000; +const P256_SIGNATURE_BYTE_LENGTH = 64; // raw ECDSA P-256 signature length + +let issuedTokenIds = 0; +/** A fresh, distinct token-id per call -- the tests only need each token to be distinguishable from the others, not any particular byte value. */ +function nextTokenId(): Uint8Array { + issuedTokenIds += 1; + return buf([issuedTokenIds]); +} + +/** Normalises to a fresh, non-shared, whole-buffer Uint8Array -- cbor2's encode() and array-literal Uint8Array construction both produce the broader Uint8Array, which the generated schemas' concrete Uint8Array fields correctly reject. */ +function buf(bytes: Uint8Array | ArrayLike): Uint8Array { + return Uint8Array.from(bytes); +} + +function encodeBuf(value: unknown): Uint8Array { + return buf(encode(value, cdeEncodeOptions)); +} + +async function generateEs256Identity(): Promise { + const keyPair = await webcrypto.subtle.generateKey( + { name: "ECDSA", namedCurve: "P-256" }, + true, + ["sign", "verify"], + ); + const publicKeyBytes = new Uint8Array( + await webcrypto.subtle.exportKey("raw", keyPair.publicKey), + ); + return createNodeIdentity(keyPair.privateKey, publicKeyBytes, ES256); +} + +function fixedClock(atMs: number): Clock { + return { now: () => atMs }; +} + +const neverRevoked: RevocationCheck = { + isRevoked: async () => Promise.resolve(false), +}; + +interface TokenSeed { + tokenId: Uint8Array; + bearer: DeviceId; + scope: CapabilityScope; + expires: number; + parent?: Uint8Array; +} + +/** Builds and signs one capability token as `identity` -- explicit field-by-field construction rather than spreading a partial claims object, since TokenClaims' own `.catchall(z.unknown())` index signature (the spec's forward-compatible extension-field pattern) makes a spread-based Omit lose the specific field types. */ +async function signToken( + identity: IdentityPort, + seed: TokenSeed, +): Promise { + const claims: TokenClaims = { + "token-id": seed.tokenId, + issuer: identity.deviceId, + "issuer-key": identity.identityKey, + bearer: seed.bearer, + capability: "exec:pty", + scope: seed.scope, + expires: seed.expires, + ...(seed.parent !== undefined ? { parent: seed.parent } : {}), + }; + + const payload = encodeBuf(claims); + const protectedHeader = encodeBuf({}); + const toBeSigned = encodeBuf([ + "Signature1", + protectedHeader, + new Uint8Array(0), + payload, + ]); + const signature = await identity.sign(toBeSigned); + return [protectedHeader, {}, payload, signature]; +} + +describe("verifyCapabilityToken", () => { + let issuer: IdentityPort; + let bearerIdentity: IdentityPort; + let bearerDeviceId: DeviceId; + + beforeAll(async () => { + issuer = await generateEs256Identity(); + // Kept in full, not just its device-id: a delegated token's own self-certifying issuer-key must belong to whichever identity actually signs it, so the tests that have this device delegate a narrower token need to sign as it, not merely reference its device-id. + bearerIdentity = await generateEs256Identity(); + bearerDeviceId = bearerIdentity.deviceId; + }); + + const now = 1_893_456_000_000; + const workScope: CapabilityScope = { kind: "folder", path: "/work" }; + + it("accepts a validly signed, unexpired, unrevoked token", async () => { + const token = await signToken(issuer, { + tokenId: nextTokenId(), + bearer: bearerDeviceId, + scope: workScope, + expires: now + HOUR_MS, + }); + + const verdict = await verifyCapabilityToken(token, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict.ok).toBe(true); + }); + + it("rejects a token whose signature doesn't match its claimed issuer", async () => { + const token = await signToken(issuer, { + tokenId: nextTokenId(), + bearer: bearerDeviceId, + scope: workScope, + expires: now + HOUR_MS, + }); + const tampered: CapabilityToken = [ + token[0], + token[1], + token[2], + new Uint8Array(P256_SIGNATURE_BYTE_LENGTH), + ]; + + const verdict = await verifyCapabilityToken(tampered, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ ok: false, reason: "bad_signature" }); + }); + + it("rejects an expired token", async () => { + const token = await signToken(issuer, { + tokenId: nextTokenId(), + bearer: bearerDeviceId, + scope: workScope, + expires: now - 1, + }); + + const verdict = await verifyCapabilityToken(token, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ ok: false, reason: "expired" }); + }); + + it("rejects a revoked token", async () => { + const token = await signToken(issuer, { + tokenId: nextTokenId(), + bearer: bearerDeviceId, + scope: workScope, + expires: now + HOUR_MS, + }); + const revoked: RevocationCheck = { + isRevoked: async () => Promise.resolve(true), + }; + + const verdict = await verifyCapabilityToken(token, { + identity: issuer, + clock: fixedClock(now), + revocation: revoked, + }); + + expect(verdict).toEqual({ ok: false, reason: "revoked" }); + }); + + it("rejects a token presented by a device other than its bearer", async () => { + const token = await signToken(issuer, { + tokenId: nextTokenId(), + bearer: bearerDeviceId, + scope: workScope, + expires: now + HOUR_MS, + }); + const someoneElse = (await generateEs256Identity()).deviceId; + + const verdict = await verifyCapabilityToken(token, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + expectedBearer: someoneElse, + }); + + expect(verdict).toEqual({ ok: false, reason: "bearer_mismatch" }); + }); + + it("accepts a delegated token whose expiry narrows the parent's", async () => { + const rootExpiry = now + 2 * HOUR_MS; + const root = await signToken(issuer, { + tokenId: nextTokenId(), + bearer: bearerDeviceId, + scope: workScope, + expires: rootExpiry, + }); + + const delegate = await generateEs256Identity(); + // Signed by bearerIdentity, not issuer: the root's bearer is the one delegating, so it must be the actual signer of the child token -- a self-certifying token's issuer-key has to belong to whoever actually signed it (checked independently by "wrong_issuer"), and delegation requires the child's issuer to equal the parent's bearer. + const claims: TokenClaims = { + "token-id": nextTokenId(), + issuer: bearerDeviceId, + "issuer-key": bearerIdentity.identityKey, + bearer: delegate.deviceId, + capability: "exec:pty", + scope: { kind: "folder", path: "/work/subdir" }, + expires: now + HOUR_MS, + parent: encodeBuf(root), + }; + const payload = encodeBuf(claims); + const protectedHeader = encodeBuf({}); + const toBeSigned = encodeBuf([ + "Signature1", + protectedHeader, + new Uint8Array(0), + payload, + ]); + const signature = await bearerIdentity.sign(toBeSigned); + const delegated: CapabilityToken = [ + protectedHeader, + {}, + payload, + signature, + ]; + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict.ok).toBe(true); + }); + + it("rejects a delegated token whose expiry exceeds its parent's", async () => { + const rootExpiry = now + HOUR_MS; + const root = await signToken(issuer, { + tokenId: nextTokenId(), + bearer: bearerDeviceId, + scope: workScope, + expires: rootExpiry, + }); + + const delegate = await generateEs256Identity(); + const claims: TokenClaims = { + "token-id": nextTokenId(), + issuer: bearerDeviceId, + "issuer-key": bearerIdentity.identityKey, + bearer: delegate.deviceId, + capability: "exec:pty", + scope: { kind: "folder", path: "/work/subdir" }, + expires: rootExpiry + HOUR_MS, // wider than the parent -- must be rejected + parent: encodeBuf(root), + }; + const payload = encodeBuf(claims); + const protectedHeader = encodeBuf({}); + const toBeSigned = encodeBuf([ + "Signature1", + protectedHeader, + new Uint8Array(0), + payload, + ]); + const signature = await bearerIdentity.sign(toBeSigned); + const widened: CapabilityToken = [protectedHeader, {}, payload, signature]; + + const verdict = await verifyCapabilityToken(widened, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ ok: false, reason: "delegation_exceeds_parent" }); + }); +}); + +describe("createMemoryStorage / createSystemClock", () => { + it("round-trips a value through memory storage", async () => { + const storage = createMemoryStorage(); + const sampleBytes = new TextEncoder().encode("sample"); + await storage.set("k", sampleBytes); + expect(await storage.get("k")).toEqual(sampleBytes); + await storage.delete("k"); + expect(await storage.get("k")).toBeUndefined(); + }); + + it("lists keys by prefix", async () => { + const storage = createMemoryStorage(); + await storage.set("a/1", new Uint8Array()); + await storage.set("a/2", new Uint8Array()); + await storage.set("b/1", new Uint8Array()); + expect(new Set(await storage.keys("a/"))).toEqual(new Set(["a/1", "a/2"])); + }); + + it("reports the current time", () => { + const clock = createSystemClock(); + const before = Date.now(); + const reported = clock.now(); + const after = Date.now(); + expect(reported).toBeGreaterThanOrEqual(before); + expect(reported).toBeLessThanOrEqual(after); + }); +}); diff --git a/ts/packages/core/tsconfig.json b/ts/packages/core/tsconfig.json new file mode 100644 index 0000000..1ee9227 --- /dev/null +++ b/ts/packages/core/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "nodenext", + "moduleResolution": "nodenext", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noEmit": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": [ + "src/generated/protocol.ts", + "src/generated/runtime.ts", + "src/ports/transport.ts", + "src/ports/storage.ts", + "src/ports/identity.ts", + "src/ports/clock.ts", + "src/domain/handshake.ts", + "src/domain/tokens.ts", + "src/adapters/tcp-transport.ts", + "src/adapters/memory-storage.ts", + "src/adapters/node-identity.ts", + "src/adapters/system-clock.ts" + ] +} diff --git a/ts/packages/core/tsconfig.node.json b/ts/packages/core/tsconfig.node.json new file mode 100644 index 0000000..ba5ab20 --- /dev/null +++ b/ts/packages/core/tsconfig.node.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true + }, + "include": ["generate.ts", "tsdown.config.ts", "eslint.config.ts", "test/**/*.ts"] +} diff --git a/ts/packages/core/tsdown.config.ts b/ts/packages/core/tsdown.config.ts new file mode 100644 index 0000000..3d93d97 --- /dev/null +++ b/ts/packages/core/tsdown.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "tsdown"; + +// Every real module gets its own build entry rather than a re-exporting index.ts -- the barrel-policy lint rule requires importing straight from the module that owns each export. src/generated/protocol.ts is committed, machine-generated output (see generate.ts); it's built here like any other module (real code that needs to compile and bundle correctly) but excluded from eslint's own strict authored-code rules (see eslint.config.ts's ignores) since rules like no-use-before-define don't make sense for its z.lazy()-wrapped mutually-recursive schemas. +export default defineConfig({ + entry: [ + "src/generated/protocol.ts", + "src/generated/runtime.ts", + "src/ports/transport.ts", + "src/ports/storage.ts", + "src/ports/identity.ts", + "src/ports/clock.ts", + "src/domain/handshake.ts", + "src/domain/tokens.ts", + "src/adapters/tcp-transport.ts", + "src/adapters/memory-storage.ts", + "src/adapters/node-identity.ts", + "src/adapters/system-clock.ts", + ], + format: ["esm", "cjs"], + dts: true, + exports: true, + attw: { profile: "node16" }, + clean: true, +}); diff --git a/ts/packages/core/turbo.json b/ts/packages/core/turbo.json new file mode 100644 index 0000000..dbad2d5 --- /dev/null +++ b/ts/packages/core/turbo.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://turborepo.com/schema.json", + "extends": ["//"], + + "tasks": { + "_build": { + "inputs": ["src/**", "tsdown.config.ts", "tsconfig.json"], + "outputs": ["dist/**"] + }, + "_generate": { + "dependsOn": ["_build"], + "inputs": ["generate.ts"], + "outputs": ["src/generated/protocol.ts"] + }, + "_test": { + "dependsOn": ["_build"] + }, + "_typecheck": { + "dependsOn": ["_build"], + "inputs": ["**/*.ts", "tsconfig.json", "tsconfig.node.json"] + }, + "_lint": { + "dependsOn": ["_build"], + "inputs": ["$TURBO_DEFAULT$", "eslint.config.ts"], + "outputs": [".eslintcache"] + }, + "_conformance-check": { + "dependsOn": ["_build"] + } + } +} diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml new file mode 100644 index 0000000..c93d097 --- /dev/null +++ b/ts/pnpm-lock.yaml @@ -0,0 +1,2579 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + turbo: + specifier: 2.10.12 + version: 2.10.12 + + packages/core: + dependencies: + cbor2: + specifier: 2.3.0 + version: 2.3.0 + cddl.js: + specifier: github:ExaDev/cddl.js#main + version: https://codeload.github.com/ExaDev/cddl.js/tar.gz/5ea7ef6578e3c08bcccd619b9698cd3d3d6d423d + zod: + specifier: 4.5.4 + version: 4.5.4 + devDependencies: + '@arethetypeswrong/cli': + specifier: 0.18.5 + version: 0.18.5 + '@exadev/eslint-config': + specifier: 2.10.6 + version: 2.10.6(eslint@10.10.0(jiti@2.7.0))(typescript-eslint@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3))(typescript@6.0.3) + '@types/node': + specifier: 26.4.1 + version: 26.4.1 + eslint: + specifier: 10.10.0 + version: 10.10.0(jiti@2.7.0) + eslint-config-prettier: + specifier: 10.1.8 + version: 10.1.8(eslint@10.10.0(jiti@2.7.0)) + eslint-plugin-prettier: + specifier: 5.5.6 + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)))(eslint@10.10.0(jiti@2.7.0))(prettier@3.9.6) + globals: + specifier: 17.12.0 + version: 17.12.0 + jiti: + specifier: 2.7.0 + version: 2.7.0 + prettier: + specifier: 3.9.6 + version: 3.9.6 + tsdown: + specifier: 0.23.0 + version: 0.23.0(@arethetypeswrong/core@0.18.5)(typescript@6.0.3) + turbo: + specifier: 2.10.12 + version: 2.10.12 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vitest: + specifier: 5.0.0 + version: 5.0.0(@types/node@26.4.1)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + +packages: + + '@andrewbranch/untar.js@1.0.4': + resolution: {integrity: sha512-pVXSwPsLuw8IGLo2Di0EaOfsk+ntVvpkk942J/sHYIkwvtKUakEcPh7HBgZ6tuimgzKSEHgCvO4XgQ05DEbwDw==} + + '@arethetypeswrong/cli@0.18.5': + resolution: {integrity: sha512-gM+8vRsQOD/Uc7EnBedUhkG5OCsDWE4uoak5QvomGpMpaky0Eh41p04nIMgrWb8EOmqZUJGc6zz9hsP6E56R7g==} + engines: {node: '>=20'} + hasBin: true + + '@arethetypeswrong/core@0.18.5': + resolution: {integrity: sha512-9ytjzGwxjm9Uz7I9avfbt5vlQt6uk9uRRESzJjqrznl6WKvI6dwYTo+vJ3U02Wrq/mR3iql/PzhvHhKdJIAjDQ==} + engines: {node: '>=20'} + + '@braidai/lang@1.1.2': + resolution: {integrity: sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==} + + '@cacheable/memory@2.2.0': + resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} + + '@cacheable/utils@2.5.0': + resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@cto.af/wtf8@0.0.5': + resolution: {integrity: sha512-LfUFi+Vv4eDzj+XAtR89e3wwjXA/NZjUSwU5NhwbBrLecxPaBYFy3exCuc1j+D4UZeOVdqlsl8G7LmOt18V0tg==} + engines: {node: '>=20'} + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.3': + resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@exadev/eslint-config@2.10.6': + resolution: {integrity: sha512-iU/0uHNqo9GLLpLzlBA9KN3233aKHOj7bQYsc4in1HCZaUHqjNmAdX8bPs99SxhNjsP7wOpBl7Inrh6w3JDlqw==} + engines: {node: '>=20'} + peerDependencies: + '@next/eslint-plugin-next': ^16.3.2 + eslint: '>=10.0.0' + eslint-plugin-jsx-a11y: ^6.10.2 + eslint-plugin-react: ^7.37.5 + eslint-plugin-react-hooks: ^7.1.1 + typescript: '>=4.8.4' + typescript-eslint: '>=8.0.0' + peerDependenciesMeta: + '@next/eslint-plugin-next': + optional: true + eslint-plugin-jsx-a11y: + optional: true + eslint-plugin-react: + optional: true + eslint-plugin-react-hooks: + optional: true + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@loaderkit/resolve@1.0.6': + resolution: {integrity: sha512-G8FdIoF5CypfwmD9rl8BXod5HDn8JqB0CCNBXDTaRZ+yRYhARrrSToX1zg1zy9jX3zLqigsELwhT4gNtkdQAUg==} + + '@oxc-project/types@0.148.0': + resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} + + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rolldown/binding-android-arm-eabi@1.2.7': + resolution: {integrity: sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.7': + resolution: {integrity: sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.7': + resolution: {integrity: sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.7': + resolution: {integrity: sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.7': + resolution: {integrity: sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + resolution: {integrity: sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.7': + resolution: {integrity: sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.7': + resolution: {integrity: sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + resolution: {integrity: sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.7': + resolution: {integrity: sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.7': + resolution: {integrity: sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.7': + resolution: {integrity: sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.7': + resolution: {integrity: sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.7': + resolution: {integrity: sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.7': + resolution: {integrity: sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@turbo/darwin-64@2.10.12': + resolution: {integrity: sha512-9nKgKoF6ZOUsM+or0OtNf+TTJSfGvDNP7ZFv/ZGWVwOSCkumyctQiTeHwB4UNljHTnC41AqylgbunLDHoccNrA==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.10.12': + resolution: {integrity: sha512-H4Elb1jqTZVeIC9bbcNwjSzemZ6RegoTOVHeuV5Osirt2Z8UguTyisMEkvZjPVZgMeN9J4ERZBFad40tFnkb7w==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.10.12': + resolution: {integrity: sha512-lr7KIotukvjZwEXiFSYAeOH3BWzjFVBbSzTbv0fuGFsNukYyH0+g1hB5ecqnJkgkYU+KHEMG1edOhnjiKON1wQ==} + cpu: [x64] + os: [android, linux] + + '@turbo/linux-arm64@2.10.12': + resolution: {integrity: sha512-f0pZDTtvzB5SuNwuXBaKbZHUCMCukgc8nMlHEuvLmj91Fzec+MEbr3cAvGNor5htEDqZnO6Lxt9N/GPI/77oGA==} + cpu: [arm64] + os: [android, linux] + + '@turbo/windows-64@2.10.12': + resolution: {integrity: sha512-SDOueJRjS/QcykWf2KCRtTLmIl5YMKsLbXkXQGhDwcTXvKXZiS5ih5lBl/gkwZIpYFjqA/rAlfMzlAFcVHNe0g==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.10.12': + resolution: {integrity: sha512-0i0mVUa4kKk+/B3RwEwPMf9CB+T7ul56hn5FFHNA4VUNTOoLBEd6aNf3FaKfCatDNZ6cicCEf6if9QUTVyzzcA==} + cpu: [arm64] + os: [win32] + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@26.4.1': + resolution: {integrity: sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==} + + '@typescript-eslint/eslint-plugin@8.69.0': + resolution: {integrity: sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.69.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.69.0': + resolution: {integrity: sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.69.0': + resolution: {integrity: sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.69.0': + resolution: {integrity: sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.69.0': + resolution: {integrity: sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.69.0': + resolution: {integrity: sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.69.0': + resolution: {integrity: sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.69.0': + resolution: {integrity: sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.69.0': + resolution: {integrity: sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.69.0': + resolution: {integrity: sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/mocker@5.0.0': + resolution: {integrity: sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/spy@5.0.0': + resolution: {integrity: sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==} + + '@yuku-codegen/binding-android-arm64@0.9.3': + resolution: {integrity: sha512-viote6xAyL5cKLquV2X2wRfopSckH+msDYbaI8Hh8JAaogYs8MJZVRUbSrbsY29TaPrIFZwNRwQ8+YSxs0dkGw==} + cpu: [arm64] + os: [android] + + '@yuku-codegen/binding-darwin-arm64@0.9.3': + resolution: {integrity: sha512-y2PGOLyxc724EJ+Et/5PxGfutQuV1Z2J9cxHo6W1I5CR3nk0i03J4yPrnw6rNJOfrtlISzcc7q0RrSyfPndpIg==} + cpu: [arm64] + os: [darwin] + + '@yuku-codegen/binding-darwin-x64@0.9.3': + resolution: {integrity: sha512-xZ9UpXUOLmsrKVUp7MRXxWU3drNiilRC42OLpjWEhnOehIGVF1bZgzHcqRYJxVyU53RMrkRMsaxplkGd6Qo/ow==} + cpu: [x64] + os: [darwin] + + '@yuku-codegen/binding-freebsd-x64@0.9.3': + resolution: {integrity: sha512-RGCYSZw3VonreVTpur9iOfnbMngR2/f7UOE7gwcDx5WoHjIhtmTK9EIq9qs78ARdMFAPzKp5OzHljl5QMaGJ7g==} + cpu: [x64] + os: [freebsd] + + '@yuku-codegen/binding-linux-arm-gnu@0.9.3': + resolution: {integrity: sha512-kjIJIw39GSPTF0hnq+jnM0tR+J5WlariAAWetxMtawNskITDT1ZigaK3YF5hMhDz2mAofaM1t+63qtx+7aXjeg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-arm-musl@0.9.3': + resolution: {integrity: sha512-TVHeVdzaS4ub86URrQufiy4t01ZtdyKDZ5sTPqWFKAUbQJ7rQ0o5vIT+/jCeHQbP+Yf9p5peRdmPbcZewoDNiA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-linux-arm64-gnu@0.9.3': + resolution: {integrity: sha512-9aQeeLh1qaCa2GcyzHnwLKGfFrsI+KOyhnh4+fICSq5kyhzCWQfXVCCK4RTEZPLcyuVwfN5Id0JEYavRN4oNTQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-arm64-musl@0.9.3': + resolution: {integrity: sha512-TLXA5Hd1nr8VSP2MtxcFddsBIHj+DPpyG5eberEGMATndgG7STlKQmle51MV0MZ8UgsdYM6CybIVpzlCPQwP9w==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-linux-x64-gnu@0.9.3': + resolution: {integrity: sha512-tk0BFbF3Clb9k9biPH3qmr+Qwk24rRM26+HY91hYXmZlzlepZG0scQ6OffZFWtzwKE5JjlZpMdcWj1lTiHbEjA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-x64-musl@0.9.3': + resolution: {integrity: sha512-xo+pXshsCXruEEkDMbwHqkeTyC4XHb6A6oXr6x2YK3jOMcS5kZz0e26BmTCr40J0nY/K3ysmwG9R1xHygtiuwQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-win32-arm64@0.9.3': + resolution: {integrity: sha512-70gAQo6HZzMgIJTjeMZOEO2XaRj4GcNGP/n81R9tXQv0x8d4ZHnZDv+ygeWfHo+xchAUPwpnrVZA4p6RhJa3GQ==} + cpu: [arm64] + os: [win32] + + '@yuku-codegen/binding-win32-x64@0.9.3': + resolution: {integrity: sha512-7Hz3NGlq6qBBR8zMEk6GMZivofNjnYZpMXSoUwUgz9Ur2LaivuP23HQh0ern+T6HVkTJEvilw4VJrg1rX1Ugcg==} + cpu: [x64] + os: [win32] + + '@yuku-parser/binding-android-arm64@0.9.3': + resolution: {integrity: sha512-z3tDGTaUXD5Q4IuebFOY/QHxhR/SqujMhkMv9C5bh25upyFEXdafp0MsXQIIheWhVZzn5VMOniyxFni/7s9LgQ==} + cpu: [arm64] + os: [android] + + '@yuku-parser/binding-darwin-arm64@0.9.3': + resolution: {integrity: sha512-hzKvKSKS7z3ufnu1VkQYEoxmS6A5uNnkwukKnc2atxpWdq648nLVOqut4h1jUXjbM2WcoTfuZLF6AHAGFf8cmg==} + cpu: [arm64] + os: [darwin] + + '@yuku-parser/binding-darwin-x64@0.9.3': + resolution: {integrity: sha512-OM2PiVlPATvzlj/KGHNlu+t8FC3YzM5joVNjhsz/EaigQ8x2UUQ1Q0agQLiv5GZ2L8TSzrLUwBCZ7YOvP8q+tw==} + cpu: [x64] + os: [darwin] + + '@yuku-parser/binding-freebsd-x64@0.9.3': + resolution: {integrity: sha512-WMn9M4LHNVysGNwsAJ9gpRPqQIW2lcPrDNGcyk0agx3IYqCJq1MQugh6Be8Otc5U08eGdE39o8Kx9YWJmXz7GA==} + cpu: [x64] + os: [freebsd] + + '@yuku-parser/binding-linux-arm-gnu@0.9.3': + resolution: {integrity: sha512-vqKjwiyW1FWvbykYMEQIcAJwA17WxK3rxxqDuyCvQwcNVaLEUxI4BXiUdlF3kHucc7MnoFQhoRPkySgOzlLM+Q==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-arm-musl@0.9.3': + resolution: {integrity: sha512-qohilhYOT+zkt2gYzym4F1T6BzRdvPqS9/sFB03pmnVV8LrvjOXVsGwEBAa3CEvzJKhAZjdTmVz7zsVdjyHWLg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-linux-arm64-gnu@0.9.3': + resolution: {integrity: sha512-tvTIyUvTGkeee68i4JIo9o27As+Ug6LXOZvElGgFbTs6+KBdb5LGhvugaIQljdfIsm2XnIbOLG6OnQSXHMLB9w==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-arm64-musl@0.9.3': + resolution: {integrity: sha512-OWZBHW1wuChBpFlrF+On1yiBcFPMRrj2g0VKsM/PCBGffu1OM0ORkS+vLS3Snu6wYwndM5Dd9Ctvux6eUlrQjA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-linux-x64-gnu@0.9.3': + resolution: {integrity: sha512-/tbk1h0dlADOCngbiQO9V3SHwBIJkoGBq8BDEraSw2CC3nGxPEPTCCYUDp5738Ij2FxVwy1FWVuhFkHvpMrPbQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-x64-musl@0.9.3': + resolution: {integrity: sha512-u3+0sCso/mcjDvtc3866D2giV7l34PDyDVBkMesAWa3DWbIWPlybehi2SSnmScgArV22ctqSSgUzdBBVJ6zYAg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-win32-arm64@0.9.3': + resolution: {integrity: sha512-xnGEvdhyjRkXozHtpXVpEEkyGZWAxJ3RoILv7XRV5SvTEztaTCk5GQyfcOzI9An/EJtcL4ERCI8QmS0TpDPJiA==} + cpu: [arm64] + os: [win32] + + '@yuku-parser/binding-win32-x64@0.9.3': + resolution: {integrity: sha512-N5eShGcuwnrXEprePFmEjtng6acZmtnC4zgazK9xxkavcx1q1uX8Nz8lU5RS973EMlNr902cIc6Cd2D4tqZDBg==} + cpu: [x64] + os: [win32] + + '@yuku-toolchain/types@0.9.3': + resolution: {integrity: sha512-rFE+5P4g2wxko5C85MugJOlVjBHEQq87dIkhLkniLXLp63PEtgaFjD954i5HXlfnyzLxPcZHsSOVDVgmo1HToA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + + cacheable@2.5.0: + resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + + camelcase@9.0.0: + resolution: {integrity: sha512-TO9xmyXTZ9HUHI8M1OnvExxYB0eYVS/1e5s7IDMTAoIcwUd+aNcFODs6Xk83mobk0velyHFQgA1yIrvYc6wclw==} + engines: {node: '>=20'} + + cbor2@2.3.0: + resolution: {integrity: sha512-76WB3hq8BoaGkMkBVJ27fW5LJU+qqDLEpgRNCG/SYKhODWXpVPOTD4UcUto3IEzYLA52nsvbhb0wabhHDn3qXg==} + engines: {node: '>=20'} + + cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/5ea7ef6578e3c08bcccd619b9698cd3d3d6d423d: + resolution: {tarball: https://codeload.github.com/ExaDev/cddl.js/tar.gz/5ea7ef6578e3c08bcccd619b9698cd3d3d6d423d} + version: 0.0.0 + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dts-resolver@3.0.0: + resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} + engines: {node: ^22.18.0 || >=24.0.0} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.10.0: + resolution: {integrity: sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + + file-entry-cache@11.1.5: + resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@6.1.23: + resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-tsconfig@5.0.0-beta.6: + resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==} + engines: {node: '>=20.20.0'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@17.12.0: + resolution: {integrity: sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==} + engines: {node: '>=18'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + hookable@6.1.1: + resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.8: + resolution: {integrity: sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==} + engines: {node: '>= 4'} + + import-without-cache@0.4.0: + resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==} + engines: {node: ^22.18.0 || >=24.0.0} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + magic-string@1.2.3: + resolution: {integrity: sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==} + + marked-terminal@7.3.0: + resolution: {integrity: sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==} + engines: {node: '>=16.0.0'} + peerDependencies: + marked: '>=1 <16' + + marked@9.1.6: + resolution: {integrity: sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==} + engines: {node: '>= 16'} + hasBin: true + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rolldown-plugin-dts@0.28.5: + resolution: {integrity: sha512-yYd3C9CeJwqjOc9X23m0Tyxcqic491uLZlfg51szT287S8zCCqLR2uoySoElgqy2CLn7PdXcEo1dlkBs4n1WHg==} + engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} + peerDependencies: + '@typescript/native-preview': '*' + '@volar/typescript': ~2.4.0 + rolldown: ^1.2.0 + typescript: ^5.0.0 || ^6.0.0 || ~7.0.0 + vue-tsc: ~3.2.0 || ~3.3.0 + peerDependenciesMeta: + '@typescript/native-preview': + optional: true + '@volar/typescript': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.2.7: + resolution: {integrity: sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + engines: {node: ^14.18.0 || >=16.0.0} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@6.1.4: + resolution: {integrity: sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==} + engines: {node: '>=20.0.0'} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsdown@0.23.0: + resolution: {integrity: sha512-BaT+ep1xnj5hdyICLd5r5SYYE+TXnI1ATePLykv9vcKpHpFTWUWRH+x1AF/E2qcu3YB6+vI8IdyxIIIffEqw5Q==} + engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.23.0 + '@tsdown/exe': 0.23.0 + '@vitejs/devtools': '*' + publint: ^0.3.8 + tsx: '*' + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 + unplugin-unused: '>=0.5.0' + unrun: '*' + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + + turbo@2.10.12: + resolution: {integrity: sha512-AswgMPnpOoaVZHrrSBejETzEbuIA69OVGwfkHwfrY0A23VjWXBANzgq9+OymWOHAIArB7D1+1z498WY8fGg1Jw==} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.69.0: + resolution: {integrity: sha512-B3MltX0VqjUBNEe3b3sSuiRbfa6XrfHFtBiPamjT5AsW/dfq+y+bc0wyuS9DxAS1LyzCxRp2+rxzpLUvqM2BvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.6.1-rc: + resolution: {integrity: sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + verkit@0.4.0: + resolution: {integrity: sha512-sXMwN6DMHeouPfCxkxWkKAmxphWKEenHYY5H1nIBzU3PmDsmJp6kBXJdshjVdpMZuWCmL9SH7KFRx29AylpP6g==} + engines: {node: '>=18.12.0'} + + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@5.0.0: + resolution: {integrity: sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.0 + '@vitest/browser-preview': 5.0.0 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.0 + '@vitest/coverage-v8': 5.0.0 + '@vitest/ui': 5.0.0 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs@16.2.2: + resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==} + engines: {node: '>=10'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yuku-ast@0.9.3: + resolution: {integrity: sha512-Kt0PXlPCXdKF1fWFTl7N3DaxsVk6DHF8VAXHJMkwPtJnWYgbx20pYgGSweuC/86mIM7k1NUITQ4JffgOLUWTCw==} + + yuku-codegen@0.9.3: + resolution: {integrity: sha512-7oTWwetHiMSyrVPFb8089lBBqkU1gaeQZyumNBJ0t5hocMQRaWhnMeSXTz7J1xm/rcw9+ePsajORdZbub2C35w==} + + yuku-parser@0.9.3: + resolution: {integrity: sha512-96wPoHnwaXfkZv7UIOUkDb+s7ZH8lv8Qtg+MxdvHUV1LFa5jV9iKOaams1942YQt+krFQrWiD6lSg2ermv5kHA==} + + zod@4.5.4: + resolution: {integrity: sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==} + +snapshots: + + '@andrewbranch/untar.js@1.0.4': {} + + '@arethetypeswrong/cli@0.18.5': + dependencies: + '@arethetypeswrong/core': 0.18.5 + chalk: 4.1.2 + cli-table3: 0.6.5 + commander: 10.0.1 + marked: 9.1.6 + marked-terminal: 7.3.0(marked@9.1.6) + semver: 7.8.5 + + '@arethetypeswrong/core@0.18.5': + dependencies: + '@andrewbranch/untar.js': 1.0.4 + '@loaderkit/resolve': 1.0.6 + cjs-module-lexer: 1.4.3 + fflate: 0.8.3 + lru-cache: 11.5.2 + semver: 7.8.5 + typescript: 5.6.1-rc + validate-npm-package-name: 5.0.1 + + '@braidai/lang@1.1.2': {} + + '@cacheable/memory@2.2.0': + dependencies: + '@cacheable/utils': 2.5.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.5.0': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + + '@colors/colors@1.5.0': + optional: true + + '@cto.af/wtf8@0.0.5': {} + + '@eslint-community/eslint-utils@4.10.1(eslint@10.10.0(jiti@2.7.0))': + dependencies: + eslint: 10.10.0(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.6 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.7.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.10.0(jiti@2.7.0))': + optionalDependencies: + eslint: 10.10.0(jiti@2.7.0) + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.3': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@exadev/eslint-config@2.10.6(eslint@10.10.0(jiti@2.7.0))(typescript-eslint@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3))(typescript@6.0.3)': + dependencies: + '@eslint/js': 10.0.1(eslint@10.10.0(jiti@2.7.0)) + '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.10.0(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + typescript-eslint: 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + transitivePeerDependencies: + - supports-color + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + + '@loaderkit/resolve@1.0.6': + dependencies: + '@braidai/lang': 1.1.2 + + '@oxc-project/types@0.148.0': {} + + '@pkgr/core@0.3.6': {} + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm-eabi@1.2.7': + optional: true + + '@rolldown/binding-android-arm64@1.2.7': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.7': + optional: true + + '@rolldown/binding-darwin-x64@1.2.7': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.7': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.7': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.7': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.7': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.7': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.7': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@sindresorhus/is@4.6.0': {} + + '@turbo/darwin-64@2.10.12': + optional: true + + '@turbo/darwin-arm64@2.10.12': + optional: true + + '@turbo/linux-64@2.10.12': + optional: true + + '@turbo/linux-arm64@2.10.12': + optional: true + + '@turbo/windows-64@2.10.12': + optional: true + + '@turbo/windows-arm64@2.10.12': + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@26.4.1': + dependencies: + undici-types: 8.3.0 + + '@typescript-eslint/eslint-plugin@8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/type-utils': 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.69.0 + eslint: 10.10.0(jiti@2.7.0) + ignore: 7.0.8 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.69.0 + debug: 4.4.3 + eslint: 10.10.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.69.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.69.0(typescript@6.0.3) + '@typescript-eslint/types': 8.69.0 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.69.0': + dependencies: + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/visitor-keys': 8.69.0 + + '@typescript-eslint/tsconfig-utils@8.69.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.10.0(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.69.0': {} + + '@typescript-eslint/typescript-estree@8.69.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.69.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.69.0(typescript@6.0.3) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/visitor-keys': 8.69.0 + debug: 4.4.3 + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(typescript@6.0.3) + eslint: 10.10.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.69.0': + dependencies: + '@typescript-eslint/types': 8.69.0 + eslint-visitor-keys: 5.0.1 + + '@vitest/mocker@5.0.0(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.0 + estree-walker: 3.0.3 + magic-string: 1.2.3 + optionalDependencies: + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + + '@vitest/spy@5.0.0': {} + + '@yuku-codegen/binding-android-arm64@0.9.3': + optional: true + + '@yuku-codegen/binding-darwin-arm64@0.9.3': + optional: true + + '@yuku-codegen/binding-darwin-x64@0.9.3': + optional: true + + '@yuku-codegen/binding-freebsd-x64@0.9.3': + optional: true + + '@yuku-codegen/binding-linux-arm-gnu@0.9.3': + optional: true + + '@yuku-codegen/binding-linux-arm-musl@0.9.3': + optional: true + + '@yuku-codegen/binding-linux-arm64-gnu@0.9.3': + optional: true + + '@yuku-codegen/binding-linux-arm64-musl@0.9.3': + optional: true + + '@yuku-codegen/binding-linux-x64-gnu@0.9.3': + optional: true + + '@yuku-codegen/binding-linux-x64-musl@0.9.3': + optional: true + + '@yuku-codegen/binding-win32-arm64@0.9.3': + optional: true + + '@yuku-codegen/binding-win32-x64@0.9.3': + optional: true + + '@yuku-parser/binding-android-arm64@0.9.3': + optional: true + + '@yuku-parser/binding-darwin-arm64@0.9.3': + optional: true + + '@yuku-parser/binding-darwin-x64@0.9.3': + optional: true + + '@yuku-parser/binding-freebsd-x64@0.9.3': + optional: true + + '@yuku-parser/binding-linux-arm-gnu@0.9.3': + optional: true + + '@yuku-parser/binding-linux-arm-musl@0.9.3': + optional: true + + '@yuku-parser/binding-linux-arm64-gnu@0.9.3': + optional: true + + '@yuku-parser/binding-linux-arm64-musl@0.9.3': + optional: true + + '@yuku-parser/binding-linux-x64-gnu@0.9.3': + optional: true + + '@yuku-parser/binding-linux-x64-musl@0.9.3': + optional: true + + '@yuku-parser/binding-win32-arm64@0.9.3': + optional: true + + '@yuku-parser/binding-win32-x64@0.9.3': + optional: true + + '@yuku-toolchain/types@0.9.3': {} + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.3.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + any-promise@1.3.0: {} + + assertion-error@2.0.1: {} + + balanced-match@4.0.4: {} + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + cac@7.0.0: {} + + cacheable@2.5.0: + dependencies: + '@cacheable/memory': 2.2.0 + '@cacheable/utils': 2.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 + + camelcase@9.0.0: {} + + cbor2@2.3.0: + dependencies: + '@cto.af/wtf8': 0.0.5 + + cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/5ea7ef6578e3c08bcccd619b9698cd3d3d6d423d: + dependencies: + camelcase: 9.0.0 + cbor2: 2.3.0 + zod: 4.5.4 + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + char-regex@1.0.2: {} + + cjs-module-lexer@1.4.3: {} + + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.2 + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@10.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + defu@6.1.7: {} + + detect-libc@2.1.2: {} + + dts-resolver@3.0.0: {} + + emoji-regex@8.0.0: {} + + emojilib@2.4.0: {} + + empathic@2.0.1: {} + + environment@1.1.0: {} + + es-module-lexer@2.3.2: {} + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)): + dependencies: + eslint: 10.10.0(jiti@2.7.0) + + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)))(eslint@10.10.0(jiti@2.7.0))(prettier@3.9.6): + dependencies: + eslint: 10.10.0(jiti@2.7.0) + prettier: 3.9.6 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.13 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@10.10.0(jiti@2.7.0)) + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.10.0(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.7.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.3 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 11.1.5 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.6 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fflate@0.8.3: {} + + file-entry-cache@11.1.5: + dependencies: + flat-cache: 6.1.23 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@6.1.23: + dependencies: + cacheable: 2.5.0 + flatted: 3.4.4 + hookified: 1.15.1 + + flatted@3.4.4: {} + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + get-tsconfig@5.0.0-beta.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@17.12.0: {} + + has-flag@4.0.0: {} + + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + + highlight.js@10.7.3: {} + + hookable@6.1.1: {} + + hookified@1.15.1: {} + + hookified@2.2.0: {} + + ignore@5.3.2: {} + + ignore@7.0.8: {} + + import-without-cache@0.4.0: {} + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + jiti@2.7.0: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lru-cache@11.5.2: {} + + magic-string@1.2.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + marked-terminal@7.3.0(marked@9.1.6): + dependencies: + ansi-escapes: 7.3.0 + ansi-regex: 6.3.0 + chalk: 5.6.2 + cli-highlight: 2.1.11 + cli-table3: 0.6.5 + marked: 9.1.6 + node-emoji: 2.2.0 + supports-hyperlinks: 3.2.0 + + marked@9.1.6: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.18: {} + + natural-compare@1.4.0: {} + + node-emoji@2.2.0: + dependencies: + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + + object-assign@4.1.1: {} + + obug@2.1.4: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.9.6: {} + + punycode@2.3.1: {} + + qified@0.10.1: + dependencies: + hookified: 2.2.0 + + quansync@1.0.0: {} + + require-directory@2.1.1: {} + + resolve-pkg-maps@1.0.0: {} + + rolldown-plugin-dts@0.28.5(rolldown@1.2.7)(typescript@6.0.3): + dependencies: + dts-resolver: 3.0.0 + get-tsconfig: 5.0.0-beta.6 + obug: 2.1.4 + rolldown: 1.2.7 + yuku-ast: 0.9.3 + yuku-codegen: 0.9.3 + yuku-parser: 0.9.3 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - oxc-resolver + + rolldown@1.2.7: + dependencies: + '@oxc-project/types': 0.148.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.7 + '@rolldown/binding-android-arm64': 1.2.7 + '@rolldown/binding-darwin-arm64': 1.2.7 + '@rolldown/binding-darwin-x64': 1.2.7 + '@rolldown/binding-freebsd-x64': 1.2.7 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.7 + '@rolldown/binding-linux-arm64-gnu': 1.2.7 + '@rolldown/binding-linux-arm64-musl': 1.2.7 + '@rolldown/binding-linux-ppc64-gnu': 1.2.7 + '@rolldown/binding-linux-s390x-gnu': 1.2.7 + '@rolldown/binding-linux-x64-gnu': 1.2.7 + '@rolldown/binding-linux-x64-musl': 1.2.7 + '@rolldown/binding-openharmony-arm64': 1.2.7 + '@rolldown/binding-win32-arm64-msvc': 1.2.7 + '@rolldown/binding-win32-x64-msvc': 1.2.7 + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + skin-tone@2.0.0: + dependencies: + unicode-emoji-modifier-base: 1.0.0 + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + synckit@0.11.13: + dependencies: + '@pkgr/core': 0.3.6 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@6.1.4: {} + + tinyexec@1.3.0: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tree-kill@1.2.2: {} + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tsdown@0.23.0(@arethetypeswrong/core@0.18.5)(typescript@6.0.3): + dependencies: + cac: 7.0.0 + defu: 6.1.7 + empathic: 2.0.1 + hookable: 6.1.1 + import-without-cache: 0.4.0 + obug: 2.1.4 + picomatch: 4.0.7 + rolldown: 1.2.7 + rolldown-plugin-dts: 0.28.5(rolldown@1.2.7)(typescript@6.0.3) + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + unconfig-core: 7.5.0 + verkit: 0.4.0 + optionalDependencies: + '@arethetypeswrong/core': 0.18.5 + typescript: 6.0.3 + transitivePeerDependencies: + - '@typescript/native-preview' + - '@volar/typescript' + - oxc-resolver + - vue-tsc + + turbo@2.10.12: + optionalDependencies: + '@turbo/darwin-64': 2.10.12 + '@turbo/darwin-arm64': 2.10.12 + '@turbo/linux-64': 2.10.12 + '@turbo/linux-arm64': 2.10.12 + '@turbo/windows-64': 2.10.12 + '@turbo/windows-arm64': 2.10.12 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.69.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.69.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.10.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@5.6.1-rc: {} + + typescript@6.0.3: {} + + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + undici-types@8.3.0: {} + + unicode-emoji-modifier-base@1.0.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + validate-npm-package-name@5.0.1: {} + + verkit@0.4.0: {} + + vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.7 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.4.1 + fsevents: 2.3.3 + jiti: 2.7.0 + + vitest@5.0.0(@types/node@26.4.1)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.0(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.2.3 + obug: 2.1.4 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.1.4 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.4.1 + transitivePeerDependencies: + - msw + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + y18n@5.0.8: {} + + yargs-parser@20.2.9: {} + + yargs@16.2.2: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yocto-queue@0.1.0: {} + + yuku-ast@0.9.3: + dependencies: + '@yuku-toolchain/types': 0.9.3 + + yuku-codegen@0.9.3: + dependencies: + '@yuku-toolchain/types': 0.9.3 + optionalDependencies: + '@yuku-codegen/binding-android-arm64': 0.9.3 + '@yuku-codegen/binding-darwin-arm64': 0.9.3 + '@yuku-codegen/binding-darwin-x64': 0.9.3 + '@yuku-codegen/binding-freebsd-x64': 0.9.3 + '@yuku-codegen/binding-linux-arm-gnu': 0.9.3 + '@yuku-codegen/binding-linux-arm-musl': 0.9.3 + '@yuku-codegen/binding-linux-arm64-gnu': 0.9.3 + '@yuku-codegen/binding-linux-arm64-musl': 0.9.3 + '@yuku-codegen/binding-linux-x64-gnu': 0.9.3 + '@yuku-codegen/binding-linux-x64-musl': 0.9.3 + '@yuku-codegen/binding-win32-arm64': 0.9.3 + '@yuku-codegen/binding-win32-x64': 0.9.3 + + yuku-parser@0.9.3: + dependencies: + '@yuku-toolchain/types': 0.9.3 + yuku-ast: 0.9.3 + optionalDependencies: + '@yuku-parser/binding-android-arm64': 0.9.3 + '@yuku-parser/binding-darwin-arm64': 0.9.3 + '@yuku-parser/binding-darwin-x64': 0.9.3 + '@yuku-parser/binding-freebsd-x64': 0.9.3 + '@yuku-parser/binding-linux-arm-gnu': 0.9.3 + '@yuku-parser/binding-linux-arm-musl': 0.9.3 + '@yuku-parser/binding-linux-arm64-gnu': 0.9.3 + '@yuku-parser/binding-linux-arm64-musl': 0.9.3 + '@yuku-parser/binding-linux-x64-gnu': 0.9.3 + '@yuku-parser/binding-linux-x64-musl': 0.9.3 + '@yuku-parser/binding-win32-arm64': 0.9.3 + '@yuku-parser/binding-win32-x64': 0.9.3 + + zod@4.5.4: {} diff --git a/ts/pnpm-workspace.yaml b/ts/pnpm-workspace.yaml new file mode 100644 index 0000000..2c900d2 --- /dev/null +++ b/ts/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +packages: + - "packages/*" + +# cddl.js is a git dependency (not published to npm) with a "prepare" build script -- an ExaDev-owned package this workspace already trusts, not a third-party one, so allowing its build script is the documented exception to pnpm's default script-blocking. +onlyBuiltDependencies: + - cddl.js diff --git a/ts/turbo.json b/ts/turbo.json new file mode 100644 index 0000000..5a7db72 --- /dev/null +++ b/ts/turbo.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://turborepo.com/schema.json", + + // Root-level tasks fan out topologically into each package's own same-named task (each package defines these itself, matching conformance/'s underscore-prefixed convention so a package's own public script calling `turbo run _build` doesn't recurse into itself). + "tasks": { + "_build": { + "dependsOn": ["^_build"], + "outputs": ["dist/**"] + }, + "_test": { + "dependsOn": ["_build", "^_build"] + }, + "_typecheck": { + "dependsOn": ["_build", "^_build"] + }, + "_lint": { + "dependsOn": ["_build", "^_build"], + "outputs": [".eslintcache"] + }, + "_conformance-check": { + "dependsOn": ["_build", "^_build"] + } + } +} From 4723495578ee390928dc2730c4d3155d95c42e31 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 10:37:50 +0100 Subject: [PATCH 02/18] chore: dispatch just build/test/lint/conformance into ts/ ts/packages/core now exists, so the justfile's own "ts/ does not exist yet" no-op branches for these recipes are stale -- dispatch for real, matching how the conformance recipe already invokes rust/'s branch conditionally (still genuinely absent) and ts/'s unconditionally now. --- justfile | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/justfile b/justfile index ccbae53..8cab0c9 100644 --- a/justfile +++ b/justfile @@ -1,12 +1,10 @@ # Thin task dispatcher across the three implementation subtrees. Each recipe # just cd's into its own subtree and calls that language's native tool -- # this file has no build-graph or caching logic of its own, and isn't meant -# to. rust/ and ts/ don't exist as code yet (only spec/ and conformance/ do), -# so their build/test/lint recipes stay no-ops until they do -- the point -# right now is that the dispatcher itself exists and matches what the -# README already describes, not that there's real work for those specific -# recipes to dispatch to yet. conformance/ owns its own build/test/lint via -# turbo (see conformance/turbo.json), so its recipes here just invoke that. +# to. rust/ doesn't exist as code yet (only spec/, conformance/, and ts/ do), +# so its build/test/lint recipes stay a no-op until it does. conformance/ and +# ts/ each own their own build/test/lint via turbo (see their own turbo.json +# files), so their recipes here just invoke that. default: @just --list @@ -15,19 +13,19 @@ default: build: cd conformance && pnpm turbo run _build @if [ -d rust ]; then cd rust && cargo build; else echo "rust/ does not exist yet"; fi - @if [ -d ts ]; then cd ts && pnpm turbo run _build; else echo "ts/ does not exist yet"; fi + cd ts && pnpm turbo run _build # Test every subtree, if it exists. test: cd conformance && pnpm turbo run _test @if [ -d rust ]; then cd rust && cargo test; else echo "rust/ does not exist yet"; fi - @if [ -d ts ]; then cd ts && pnpm turbo run _test; else echo "ts/ does not exist yet"; fi + cd ts && pnpm turbo run _test # Lint every subtree, if it exists. lint: cd conformance && pnpm turbo run _lint @if [ -d rust ]; then cd rust && cargo clippy --all-targets -- -D warnings && cargo fmt --check; else echo "rust/ does not exist yet"; fi - @if [ -d ts ]; then cd ts && pnpm turbo run _lint; else echo "ts/ does not exist yet"; fi + cd ts && pnpm turbo run _lint # Regenerate spec/protocol.cddl and validate it against an RFC 8610 parser. spec: @@ -36,11 +34,12 @@ spec: # Regenerate conformance/'s golden vectors, typecheck, and verify every # vector round-trips through cbor2. turbo owns the build/generate/test/ -# typecheck task graph and caching within conformance/ itself; once rust/ -# and ts/ exist, each implementation's own conformance-check additionally -# runs against these same vector files. +# typecheck task graph and caching within conformance/ itself. Each +# implementation's own conformance-check additionally runs against these same +# vector files -- rust/'s once it exists, ts/'s (via @exadev/wire-mesh-core) +# already. conformance: cd conformance && pnpm install cd conformance && pnpm turbo run _generate _test _typecheck _lint @if [ -d rust ]; then cd rust && cargo run --bin conformance-check; else echo "rust/ does not exist yet"; fi - @if [ -d ts ]; then cd ts && pnpm conformance-check; else echo "ts/ does not exist yet"; fi + cd ts && pnpm install && pnpm conformance-check From 6283a8bc193f86cb9c8e634b64f2de446681f7b8 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 10:37:55 +0100 Subject: [PATCH 03/18] ci: verify ts/packages/core (lint, typecheck, generated-schema drift, test) Mirrors conformance-verify's own pattern: install, lint, typecheck (which also builds every package and verifies its dual ESM/CJS surface with attw), confirm generate.ts's output matches the committed src/generated/protocol.ts (regenerate and diff, the same drift check conformance/'s vector files already get), then run the test suite -- including the conformance-check that round-trips conformance/'s golden vectors through the generated schemas. Added to required-checks alongside the two existing jobs. --- .github/workflows/ci.yml | 44 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38cd665..bd68e90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,9 +105,51 @@ jobs: working-directory: conformance run: pnpm turbo run _test + ts-core-verify: + name: ts/packages/core Verify + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: pnpm/action-setup@v6 + with: + package_json_file: ts/package.json + + - uses: actions/setup-node@v7 + with: + node-version-file: .tool-versions + cache: pnpm + cache-dependency-path: ts/pnpm-lock.yaml + + - name: Install ts/ workspace + working-directory: ts + run: pnpm install --frozen-lockfile + + - name: Lint + working-directory: ts + run: pnpm turbo run _lint + + - name: Typecheck and build every package, verifying dual ESM/CJS + types with attw + working-directory: ts + run: pnpm turbo run _typecheck + + - name: Confirm generate.ts's output matches the committed generated schema + working-directory: ts/packages/core + run: | + cp src/generated/protocol.ts /tmp/protocol-committed.ts + pnpm turbo run _generate + if ! diff -u /tmp/protocol-committed.ts src/generated/protocol.ts; then + echo "::error::ts/packages/core/src/generated/protocol.ts is out of date. Run 'pnpm run generate' in ts/packages/core and commit the result -- never edit the generated file directly." + exit 1 + fi + + - name: Test, including the conformance-check round-tripping conformance/'s golden vectors through the generated schemas + working-directory: ts + run: pnpm turbo run _test + required-checks: name: Required Checks - needs: [cddl-validate, conformance-verify] + needs: [cddl-validate, conformance-verify, ts-core-verify] if: always() runs-on: ubuntu-latest steps: From c8525812d1751f0e388fa58a5ad395ddada561bd Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 10:37:59 +0100 Subject: [PATCH 04/18] docs: describe ts/packages/core in the Implementations section It's no longer "None yet" -- record what actually exists (the ports/adapters architecture, which domain logic is real vs. deferred, the conformance-check) rather than leaving the README describing code that's now there. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 89c857f..3bcce8a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ An application-neutral peer-mesh wire protocol. Any tool that speaks it — a file-sync client, an agent communication bus, a terminal broker — can be a first-class peer in the same mesh as any other tool. No single implementation is the canonical runtime. The protocol is the contract; no codebase is. -> **Status: schema written, no implementations yet.** [`spec/protocol.cddl`](spec/protocol.cddl) is a real, RFC 8610-valid schema, validated against a CDDL parser — not just prose. Nothing consumes it yet: `rust/` and `ts/packages/*` don't exist as code, only as the repository structure below. +> **Status: schema written, one implementation underway.** [`spec/protocol.cddl`](spec/protocol.cddl) is a real, RFC 8610-valid schema, validated against a CDDL parser — not just prose. `ts/packages/core` consumes it, with schema-driven Zod generation and handshake/capability-token domain logic; `rust/` and the remaining `ts/packages/*` don't exist as code yet, only as the repository structure below. ## Why this exists @@ -90,9 +90,9 @@ ts/ ## Implementations -None yet. The schema exists (`spec/protocol.cddl`), and so does `conformance/`'s golden test vector suite; `rust/` and `ts/packages/core` don't exist as code, only as the structure above. Once they do: +`ts/packages/core` exists: a ports/adapters implementation (Transport, Storage, Identity/crypto, and Clock as first-class ports) consuming Zod schemas generated from `spec/protocol.cddl` by [cddl.js](https://github.com/ExaDev/cddl.js), with real domain logic for handshake negotiation and capability-token verification (including the delegation-chain narrowing rules `tokens.cddl` documents); its `conformance-check` round-trips every vector in `conformance/`'s golden suite through the generated schemas. `rust/` doesn't exist as code yet, only as the structure above. -- **[Cascade](https://github.com/Mearman/cascade)** refactors its own hand-written protocol code onto `rust/` as an ordinary Cargo dependency, rather than maintaining a parallel implementation. +- **[Cascade](https://github.com/Mearman/cascade)** refactors its own hand-written protocol code onto `rust/` as an ordinary Cargo dependency, rather than maintaining a parallel implementation, once `rust/` exists. - **[agent-comms](https://github.com/ExaDev/agent-comms)** refactors its own wire-protocol and transport code onto `ts/packages/core` as an ordinary pnpm dependency, the same way. - **[cddl.js](https://github.com/ExaDev/cddl.js)** gives `ts/packages/core` schema-driven Zod generation from `spec/protocol.cddl`, since no CDDL-to-TypeScript tool currently exists. From c4c9c105bf02c4f60177633f6092fe5bcf94ca46 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 10:48:19 +0100 Subject: [PATCH 05/18] fix: pin cddl.js to the commit with a working git-dependency install CI failed installing cddl.js as a git dependency: its prepare script couldn't load tsdown.config.ts, since Node's native TS-stripping refuses to process a .ts file under node_modules -- exactly where a git-dependency install resolves into while prepare runs. Fixed upstream (ExaDev/cddl.js#7, tsdown.config.ts -> tsdown.config.js); this repins the lockfile to pick it up. --- ts/pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index c93d097..fa3438b 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 2.3.0 cddl.js: specifier: github:ExaDev/cddl.js#main - version: https://codeload.github.com/ExaDev/cddl.js/tar.gz/5ea7ef6578e3c08bcccd619b9698cd3d3d6d423d + version: https://codeload.github.com/ExaDev/cddl.js/tar.gz/1196b8d82b309a8c81991f5215ca9358cd382652 zod: specifier: 4.5.4 version: 4.5.4 @@ -625,8 +625,8 @@ packages: resolution: {integrity: sha512-76WB3hq8BoaGkMkBVJ27fW5LJU+qqDLEpgRNCG/SYKhODWXpVPOTD4UcUto3IEzYLA52nsvbhb0wabhHDn3qXg==} engines: {node: '>=20'} - cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/5ea7ef6578e3c08bcccd619b9698cd3d3d6d423d: - resolution: {tarball: https://codeload.github.com/ExaDev/cddl.js/tar.gz/5ea7ef6578e3c08bcccd619b9698cd3d3d6d423d} + cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/1196b8d82b309a8c81991f5215ca9358cd382652: + resolution: {tarball: https://codeload.github.com/ExaDev/cddl.js/tar.gz/1196b8d82b309a8c81991f5215ca9358cd382652} version: 0.0.0 chai@6.2.2: @@ -1882,7 +1882,7 @@ snapshots: dependencies: '@cto.af/wtf8': 0.0.5 - cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/5ea7ef6578e3c08bcccd619b9698cd3d3d6d423d: + cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/1196b8d82b309a8c81991f5215ca9358cd382652: dependencies: camelcase: 9.0.0 cbor2: 2.3.0 From 404e22cb41f4bda19e8dc499a736c00c6d493733 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 11:02:30 +0100 Subject: [PATCH 06/18] fix: pin cddl.js to the commit with committed, pre-built dist output The previous fix (a prepare script that builds cddl.js during install) still failed in CI: eslint's type-aware linting of generate.ts resolved parse/emitModule to an error type, not real type info, something never reproduced locally across four different attempts (hoisted and isolated pnpm linkers, a from-scratch store, a genuine Linux container matching CI's exact Node/pnpm versions). Fixed upstream properly instead of chasing the install-time build further: ExaDev/cddl.js#8 commits pre-built, attw-verified dist/ output and removes the prepare script entirely, so a git-dependency install ships already-correct code and type declarations with no build step, and therefore no install-time environment sensitivity, at all. --- ts/pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index fa3438b..ec87664 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 2.3.0 cddl.js: specifier: github:ExaDev/cddl.js#main - version: https://codeload.github.com/ExaDev/cddl.js/tar.gz/1196b8d82b309a8c81991f5215ca9358cd382652 + version: https://codeload.github.com/ExaDev/cddl.js/tar.gz/fbaed5ff48d47c856f554b3f3e1f37aac98e0d80 zod: specifier: 4.5.4 version: 4.5.4 @@ -625,8 +625,8 @@ packages: resolution: {integrity: sha512-76WB3hq8BoaGkMkBVJ27fW5LJU+qqDLEpgRNCG/SYKhODWXpVPOTD4UcUto3IEzYLA52nsvbhb0wabhHDn3qXg==} engines: {node: '>=20'} - cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/1196b8d82b309a8c81991f5215ca9358cd382652: - resolution: {tarball: https://codeload.github.com/ExaDev/cddl.js/tar.gz/1196b8d82b309a8c81991f5215ca9358cd382652} + cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/fbaed5ff48d47c856f554b3f3e1f37aac98e0d80: + resolution: {tarball: https://codeload.github.com/ExaDev/cddl.js/tar.gz/fbaed5ff48d47c856f554b3f3e1f37aac98e0d80} version: 0.0.0 chai@6.2.2: @@ -1882,7 +1882,7 @@ snapshots: dependencies: '@cto.af/wtf8': 0.0.5 - cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/1196b8d82b309a8c81991f5215ca9358cd382652: + cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/fbaed5ff48d47c856f554b3f3e1f37aac98e0d80: dependencies: camelcase: 9.0.0 cbor2: 2.3.0 From b4e2e639d69f9e2e4cd672d4dcec546ddc737f29 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 13:30:43 +0100 Subject: [PATCH 07/18] build: regenerate protocol schemas from the redesigned spec coordinator-frame is new; the federation link frames, mesh-id, and share-descriptor are gone; revocation-entry is now a cose-sign1 over revocation-claims; handle-claims' mailboxes field is pluralised. --- ts/packages/core/src/generated/protocol.ts | 62 +++++----------------- 1 file changed, 14 insertions(+), 48 deletions(-) diff --git a/ts/packages/core/src/generated/protocol.ts b/ts/packages/core/src/generated/protocol.ts index 807b176..b79cdd1 100644 --- a/ts/packages/core/src/generated/protocol.ts +++ b/ts/packages/core/src/generated/protocol.ts @@ -23,7 +23,7 @@ export const handleClaimsSchema = z.lazy(() => z.object({ "device-id": z.lazy(() => deviceIdSchema), "identity-key": z.lazy(() => identityKeySchema), "candidates": z.array(z.lazy(() => wireCandidateSchema)).optional(), - "mailbox": z.lazy(() => deviceIdSchema).optional(), + "mailboxes": z.array(z.lazy(() => deviceIdSchema)).optional(), "issued": z.number().int().nonnegative(), "expires": z.number().int().nonnegative(), })); @@ -84,44 +84,7 @@ export const execSessionInfoSchema = z.lazy(() => z.object({ "argv": z.array(z.string()).optional(), "cwd": z.string().optional(), })); -export const meshIdSchema = z.lazy(() => z.string()); -export const federationLinkRequestFrameSchema = z.lazy(() => z.object({ - "type": z.literal("federation-link-request"), - "local-mesh": z.lazy(() => meshIdSchema), - "local-name": z.string(), - "offered-shares": z.array(z.lazy(() => shareDescriptorSchema)), -})); -export const federationLinkAcceptFrameSchema = z.lazy(() => z.object({ - "type": z.literal("federation-link-accept"), - "remote-mesh": z.lazy(() => meshIdSchema), - "remote-name": z.string(), - "accepted-shares": z.array(z.lazy(() => shareDescriptorSchema)), -})); -export const federationLinkRejectFrameSchema = z.lazy(() => z.object({ - "type": z.literal("federation-link-reject"), - "reason": z.string(), -})); -export const shareDescriptorSchema = z.lazy(() => z.object({ - "domain": z.lazy(() => domainIdSchema), - "resource": z.lazy(() => capabilityScopeSchema), - "direction": z.union([z.literal("inbound"), z.literal("outbound"), z.literal("bidirectional")]), -})); -export const federationShareFrameSchema = z.lazy(() => z.object({ - "type": z.literal("federation-share"), - "share": z.lazy(() => shareDescriptorSchema), -})); -export const federationUnshareFrameSchema = z.lazy(() => z.object({ - "type": z.literal("federation-unshare"), - "share": z.lazy(() => shareDescriptorSchema), -})); -export const federationEnvelopeFrameSchema = z.lazy(() => z.object({ - "type": z.literal("federation-envelope"), - "origin-mesh": z.lazy(() => meshIdSchema), - "origin-device": z.lazy(() => deviceIdSchema), - "resource": z.lazy(() => capabilityScopeSchema), - "inner": z.instanceof(Uint8Array), -})); -export const frameVariantSchema = z.lazy(() => z.union([z.lazy(() => handshakeFrameSchema), z.lazy(() => pingFrameSchema), z.lazy(() => closeFrameSchema), z.lazy(() => gossipFrameSchema), z.lazy(() => candidatesFrameSchema), z.lazy(() => syncPunchFrameSchema), z.lazy(() => observedAddressFrameSchema), z.lazy(() => relayOfferFrameSchema), z.lazy(() => relayConnectFrameSchema), z.lazy(() => relayDataFrameSchema), z.lazy(() => relayInboundFrameSchema), z.lazy(() => manageRequestFrameSchema), z.lazy(() => manageResponseFrameSchema), z.lazy(() => revocationAnnounceFrameSchema), z.lazy(() => streamDataFrameSchema), z.lazy(() => streamAckFrameSchema), z.lazy(() => streamEndFrameSchema), z.lazy(() => dataHaveFrameSchema), z.lazy(() => dataRequestFrameSchema), z.lazy(() => dataEntriesFrameSchema), z.lazy(() => federationLinkRequestFrameSchema), z.lazy(() => federationLinkAcceptFrameSchema), z.lazy(() => federationLinkRejectFrameSchema), z.lazy(() => federationShareFrameSchema), z.lazy(() => federationUnshareFrameSchema), z.lazy(() => federationEnvelopeFrameSchema)])); +export const frameVariantSchema = z.lazy(() => z.union([z.lazy(() => handshakeFrameSchema), z.lazy(() => pingFrameSchema), z.lazy(() => closeFrameSchema), z.lazy(() => gossipFrameSchema), z.lazy(() => candidatesFrameSchema), z.lazy(() => syncPunchFrameSchema), z.lazy(() => observedAddressFrameSchema), z.lazy(() => relayOfferFrameSchema), z.lazy(() => relayConnectFrameSchema), z.lazy(() => relayDataFrameSchema), z.lazy(() => relayInboundFrameSchema), z.lazy(() => coordinatorFrameSchema), z.lazy(() => manageRequestFrameSchema), z.lazy(() => manageResponseFrameSchema), z.lazy(() => revocationAnnounceFrameSchema), z.lazy(() => streamDataFrameSchema), z.lazy(() => streamAckFrameSchema), z.lazy(() => streamEndFrameSchema), z.lazy(() => dataHaveFrameSchema), z.lazy(() => dataRequestFrameSchema), z.lazy(() => dataEntriesFrameSchema)])); export const frameSchema = z.lazy(() => z.lazy(() => frameVariantSchema)); export const protocolVersionSchema = z.lazy(() => z.number().int().nonnegative()); export const domainIdSchema = z.lazy(() => z.union([z.lazy(() => coreDomainNameSchema), z.lazy(() => namespacedDomainIdSchema), z.lazy(() => privateUseDomainIdSchema)])); @@ -170,10 +133,13 @@ export const manageResponseFrameSchema = z.lazy(() => z.object({ "request-id": z.number().int().nonnegative(), "outcome": z.union([z.lazy(() => manageOkSchema), z.lazy(() => manageErrorSchema)]), })); -export const revocationEntrySchema = z.lazy(() => z.object({ +export const revocationClaimsSchema = z.lazy(() => z.object({ "token-id": z.instanceof(Uint8Array), + "issuer": z.lazy(() => deviceIdSchema), + "issuer-key": z.lazy(() => identityKeySchema), "revoked-at": z.number().int().nonnegative(), })); +export const revocationEntrySchema = z.lazy(() => z.lazy(() => coseSign1Schema)); export const revocationAnnounceFrameSchema = z.lazy(() => z.object({ "type": z.literal("revocation-announce"), "entries": z.array(z.lazy(() => revocationEntrySchema)), @@ -277,6 +243,12 @@ export const relayInboundFrameSchema = z.lazy(() => z.object({ "type": z.literal("relay-inbound"), "source-device": z.lazy(() => deviceIdSchema), })); +export const coordinatorFrameSchema = z.lazy(() => z.object({ + "type": z.literal("coordinator"), + "term": z.number().int().nonnegative(), + "coordinator": z.lazy(() => deviceIdSchema), + "capacity-hint": z.number().int().nonnegative().optional(), +})); export type DataHaveFrame = z.infer; export type DataRequestFrame = z.infer; @@ -293,14 +265,6 @@ export type ProcSignal = z.infer; export type ProcKill = z.infer; export type ExecList = z.infer; export type ExecSessionInfo = z.infer; -export type MeshId = z.infer; -export type FederationLinkRequestFrame = z.infer; -export type FederationLinkAcceptFrame = z.infer; -export type FederationLinkRejectFrame = z.infer; -export type ShareDescriptor = z.infer; -export type FederationShareFrame = z.infer; -export type FederationUnshareFrame = z.infer; -export type FederationEnvelopeFrame = z.infer; export type FrameVariant = z.infer; export type Frame = z.infer; export type ProtocolVersion = z.infer; @@ -317,6 +281,7 @@ export type ManageRequestFrame = z.infer; export type ManageOk = z.infer; export type ManageError = z.infer; export type ManageResponseFrame = z.infer; +export type RevocationClaims = z.infer; export type RevocationEntry = z.infer; export type RevocationAnnounceFrame = z.infer; export type StreamSession = z.infer; @@ -348,3 +313,4 @@ export type RelayOfferFrame = z.infer; export type RelayConnectFrame = z.infer; export type RelayDataFrame = z.infer; export type RelayInboundFrame = z.infer; +export type CoordinatorFrame = z.infer; From 010707f6d485a46d699b67a546ccbdeb4f20be4b Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 13:30:53 +0100 Subject: [PATCH 08/18] feat: enforce issuer-matched revocation and verify gossiped revocation entries RevocationCheck.isRevoked now receives the token's own issuer alongside its token-id, so an entry only counts when BOTH match -- only a token's own issuer may revoke it, and a third party's entry for someone else's token-id is ignored (the unsigned {token-id, revoked-at} shape management.cddl replaced let any peer falsely revoke any token). New verifyRevocationEntry ingests a gossiped revocation-announce entry: signature against its own embedded issuer-key, self-certifying issuer check, schema-parsed claims -- entries failing any check are dropped rather than stored. The ancestor-chain sweep (every parent's token-id checked, not just the leaf's) already falls out of verifyCapabilityToken's existing recursion. --- ts/packages/core/src/domain/tokens.ts | 60 ++++++- ts/packages/core/test/tokens.test.ts | 246 +++++++++++++++++++++++++- 2 files changed, 299 insertions(+), 7 deletions(-) diff --git a/ts/packages/core/src/domain/tokens.ts b/ts/packages/core/src/domain/tokens.ts index 2b644c1..7b9c2f7 100644 --- a/ts/packages/core/src/domain/tokens.ts +++ b/ts/packages/core/src/domain/tokens.ts @@ -1,16 +1,22 @@ import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2"; import { capabilityTokenSchema, + revocationClaimsSchema, tokenClaimsSchema, type CapabilityToken, type DeviceId, + type RevocationClaims, + type RevocationEntry, type TokenClaims, } from "../generated/protocol.js"; import type { Clock } from "../ports/clock.js"; import type { IdentityPort } from "../ports/identity.js"; +/** + * The revocation view a verifier consults. Contract per management.cddl: an entry counts against a token only when BOTH its token-id and its issuer match the token's own -- only a token's own issuer may revoke it, so a third party's entry for someone else's token-id must be ignored. Implementations ingest gossiped revocation-announce frames via verifyRevocationEntry (which enforces each entry's own signature and self-certification) and key the resulting claims by token-id + issuer. + */ export interface RevocationCheck { - isRevoked: (tokenId: Uint8Array) => Promise; + isRevoked: (tokenId: Uint8Array, issuer: DeviceId) => Promise; } export type TokenVerdictReason = @@ -97,7 +103,7 @@ export async function verifyCapabilityToken( return { ok: false, reason: "not_yet_valid" }; } - if (await options.revocation.isRevoked(claims["token-id"])) { + if (await options.revocation.isRevoked(claims["token-id"], claims.issuer)) { return { ok: false, reason: "revoked" }; } @@ -131,3 +137,53 @@ export async function verifyCapabilityToken( return { ok: true, claims }; } + +export type RevocationEntryVerdictReason = + "malformed" | "bad_signature" | "wrong_issuer"; + +export type RevocationEntryVerdict = + | { ok: true; claims: RevocationClaims } + | { ok: false; reason: RevocationEntryVerdictReason }; + +export interface VerifyRevocationEntryOptions { + /** Crypto primitives only -- any IdentityPort instance can verify any entry, since everything needed to check one travels inside the entry itself. */ + identity: IdentityPort; +} + +/** + * Verifies one gossiped revocation-entry (management.cddl): a well-formed COSE_Sign1 whose signature verifies against its own embedded issuer-key, where that issuer-key is self-certifying (sha256(issuer-key.public-key) equals the claimed issuer device-id). A verifier that ingests a revocation-announce frame runs each entry through this before recording it in its revocation view; entries failing here are dropped, not stored. The issuer-match against a specific token's own issuer (only a token's own issuer may revoke it) is deliberately NOT checked here -- it happens at lookup time in RevocationCheck, against whichever token is being verified. + */ +export async function verifyRevocationEntry( + entry: RevocationEntry, + options: VerifyRevocationEntryOptions, +): Promise { + const [protectedHeader, , payload, signature] = entry; + if (payload === null) { + return { ok: false, reason: "malformed" }; + } + + const decodedClaims: unknown = decode(payload, cdeDecodeOptions); + const claimsResult = revocationClaimsSchema.safeParse(decodedClaims); + if (!claimsResult.success) { + return { ok: false, reason: "malformed" }; + } + const claims = claimsResult.data; + + const signatureOk = await options.identity.verify( + claims["issuer-key"], + sig1ToBeSigned(protectedHeader, payload), + signature, + ); + if (!signatureOk) { + return { ok: false, reason: "bad_signature" }; + } + + const derivedIssuerId = await options.identity.deriveDeviceId( + claims["issuer-key"]["public-key"], + ); + if (!bytesEqual(derivedIssuerId, claims.issuer)) { + return { ok: false, reason: "wrong_issuer" }; + } + + return { ok: true, claims }; +} diff --git a/ts/packages/core/test/tokens.test.ts b/ts/packages/core/test/tokens.test.ts index f652824..51e3ec8 100644 --- a/ts/packages/core/test/tokens.test.ts +++ b/ts/packages/core/test/tokens.test.ts @@ -6,6 +6,7 @@ import { createMemoryStorage } from "../src/adapters/memory-storage.js"; import { createSystemClock } from "../src/adapters/system-clock.js"; import { verifyCapabilityToken, + verifyRevocationEntry, type RevocationCheck, } from "../src/domain/tokens.js"; import type { IdentityPort } from "../src/ports/identity.js"; @@ -14,12 +15,16 @@ import type { CapabilityScope, CapabilityToken, DeviceId, + RevocationClaims, + RevocationEntry, TokenClaims, } from "../src/generated/protocol.js"; const ES256 = -7; const HOUR_MS = 3_600_000; +const REVOKED_SHORTLY_BEFORE_NOW_MS = 1_000; // revoked-at sits just before `now` in these tests -- the value only needs to be in the past, not any particular distance const P256_SIGNATURE_BYTE_LENGTH = 64; // raw ECDSA P-256 signature length +const LOW_BYTE_MASK = 0xff; // XOR operand keeping the corrupted byte within one octet when tampering with a signature in tests let issuedTokenIds = 0; /** A fresh, distinct token-id per call -- the tests only need each token to be distinguishable from the others, not any particular byte value. */ @@ -53,10 +58,51 @@ function fixedClock(atMs: number): Clock { return { now: () => atMs }; } +function equalBytes(a: Uint8Array, b: Uint8Array): boolean { + return a.length === b.length && a.every((byte, i) => byte === b[i]); +} + const neverRevoked: RevocationCheck = { isRevoked: async () => Promise.resolve(false), }; +/** A RevocationCheck over an explicit set of already-verified revocation claims, applying the port's own contract: an entry only counts against a token when BOTH its token-id and its issuer match the token's own -- a revocation signed by some third party must not revoke someone else's token. */ +function revocationView(entries: readonly RevocationClaims[]): RevocationCheck { + return { + isRevoked: async (tokenId, issuer) => + Promise.resolve( + entries.some( + (entry) => + equalBytes(entry["token-id"], tokenId) && + equalBytes(entry.issuer, issuer), + ), + ), + }; +} + +/** Builds and signs one revocation-entry (a cose-sign1 over revocation-claims) as `identity`, mirroring signToken's construction. */ +async function signRevocationEntry( + identity: IdentityPort, + tokenId: Uint8Array, +): Promise { + const claims: RevocationClaims = { + "token-id": tokenId, + issuer: identity.deviceId, + "issuer-key": identity.identityKey, + "revoked-at": 0, + }; + const payload = encodeBuf(claims); + const protectedHeader = encodeBuf({}); + const toBeSigned = encodeBuf([ + "Signature1", + protectedHeader, + new Uint8Array(0), + payload, + ]); + const signature = await identity.sign(toBeSigned); + return [protectedHeader, {}, payload, signature]; +} + interface TokenSeed { tokenId: Uint8Array; bearer: DeviceId; @@ -165,26 +211,56 @@ describe("verifyCapabilityToken", () => { expect(verdict).toEqual({ ok: false, reason: "expired" }); }); - it("rejects a revoked token", async () => { + it("rejects a token revoked by its own issuer's entry", async () => { + const tokenId = nextTokenId(); const token = await signToken(issuer, { - tokenId: nextTokenId(), + tokenId, bearer: bearerDeviceId, scope: workScope, expires: now + HOUR_MS, }); - const revoked: RevocationCheck = { - isRevoked: async () => Promise.resolve(true), + const ownIssuerEntry: RevocationClaims = { + "token-id": tokenId, + issuer: issuer.deviceId, + "issuer-key": issuer.identityKey, + "revoked-at": now - REVOKED_SHORTLY_BEFORE_NOW_MS, }; const verdict = await verifyCapabilityToken(token, { identity: issuer, clock: fixedClock(now), - revocation: revoked, + revocation: revocationView([ownIssuerEntry]), }); expect(verdict).toEqual({ ok: false, reason: "revoked" }); }); + it("accepts a token whose revocation entry was signed by a third party, not its own issuer", async () => { + const tokenId = nextTokenId(); + const token = await signToken(issuer, { + tokenId, + bearer: bearerDeviceId, + scope: workScope, + expires: now + HOUR_MS, + }); + // Same token-id, but revoked-at attributed to a different issuer -- only a token's own issuer may revoke it, so this entry must not count. + const thirdParty = await generateEs256Identity(); + const thirdPartyEntry: RevocationClaims = { + "token-id": tokenId, + issuer: thirdParty.deviceId, + "issuer-key": thirdParty.identityKey, + "revoked-at": now - REVOKED_SHORTLY_BEFORE_NOW_MS, + }; + + const verdict = await verifyCapabilityToken(token, { + identity: issuer, + clock: fixedClock(now), + revocation: revocationView([thirdPartyEntry]), + }); + + expect(verdict.ok).toBe(true); + }); + it("rejects a token presented by a device other than its bearer", async () => { const token = await signToken(issuer, { tokenId: nextTokenId(), @@ -289,6 +365,166 @@ describe("verifyCapabilityToken", () => { expect(verdict).toEqual({ ok: false, reason: "delegation_exceeds_parent" }); }); + + it("rejects a delegated token whose ancestor is revoked, even though the leaf itself is not", async () => { + const rootTokenId = nextTokenId(); + const root = await signToken(issuer, { + tokenId: rootTokenId, + bearer: bearerDeviceId, + scope: workScope, + expires: now + 2 * HOUR_MS, + }); + + const delegate = await generateEs256Identity(); + const claims: TokenClaims = { + "token-id": nextTokenId(), + issuer: bearerDeviceId, + "issuer-key": bearerIdentity.identityKey, + bearer: delegate.deviceId, + capability: "exec:pty", + scope: { kind: "folder", path: "/work/subdir" }, + expires: now + HOUR_MS, + parent: encodeBuf(root), + }; + const payload = encodeBuf(claims); + const protectedHeader = encodeBuf({}); + const toBeSigned = encodeBuf([ + "Signature1", + protectedHeader, + new Uint8Array(0), + payload, + ]); + const signature = await bearerIdentity.sign(toBeSigned); + const delegated: CapabilityToken = [ + protectedHeader, + {}, + payload, + signature, + ]; + + // Only the ROOT is revoked, by the root's own issuer -- the sweep must reach it through the delegation chain, not stop at the leaf. + const rootRevokedByIssuer: RevocationClaims = { + "token-id": rootTokenId, + issuer: issuer.deviceId, + "issuer-key": issuer.identityKey, + "revoked-at": now - REVOKED_SHORTLY_BEFORE_NOW_MS, + }; + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: revocationView([rootRevokedByIssuer]), + }); + + expect(verdict).toEqual({ ok: false, reason: "parent_invalid" }); + }); +}); + +describe("verifyRevocationEntry", () => { + let issuer: IdentityPort; + + beforeAll(async () => { + issuer = await generateEs256Identity(); + }); + + it("accepts a validly signed, self-certifying revocation entry and returns its claims", async () => { + const tokenId = nextTokenId(); + const entry = await signRevocationEntry(issuer, tokenId); + + const verdict = await verifyRevocationEntry(entry, { identity: issuer }); + + expect(verdict.ok).toBe(true); + if (verdict.ok) { + expect(equalBytes(verdict.claims["token-id"], tokenId)).toBe(true); + expect(equalBytes(verdict.claims.issuer, issuer.deviceId)).toBe(true); + } + }); + + it("rejects an entry whose signature doesn't verify against its embedded issuer-key", async () => { + const entry = await signRevocationEntry(issuer, nextTokenId()); + // Corrupt the signature bytes themselves: the identity port only supplies crypto primitives and checks against the entry's own embedded key, so swapping port instances proves nothing (the neighbouring test covers exactly that) -- a genuinely bad signature must be forged in the bytes. + const [protectedHeader, unprotected, payload, signature] = entry; + const tampered = Uint8Array.from(signature); + const firstByte = tampered.at(0); + if (firstByte === undefined) { + throw new Error("test setup: signature has no bytes to corrupt"); + } + tampered[0] = firstByte ^ LOW_BYTE_MASK; + const forged: RevocationEntry = [ + protectedHeader, + unprotected, + payload, + tampered, + ]; + + const verdict = await verifyRevocationEntry(forged, { identity: issuer }); + + expect(verdict).toEqual({ ok: false, reason: "bad_signature" }); + }); + + it("rejects an entry whose issuer-key does not derive its claimed issuer device-id", async () => { + // Sign as one identity but claim a different issuer: the signature verifies (it was really signed by the embedded key) but sha256(public-key) != the claimed issuer, so the entry is not self-certifying. + const actualSigner = await generateEs256Identity(); + const claimedIssuer = await generateEs256Identity(); + const claims: RevocationClaims = { + "token-id": nextTokenId(), + issuer: claimedIssuer.deviceId, + "issuer-key": actualSigner.identityKey, + "revoked-at": 0, + }; + const payload = encodeBuf(claims); + const protectedHeader = encodeBuf({}); + const toBeSigned = encodeBuf([ + "Signature1", + protectedHeader, + new Uint8Array(0), + payload, + ]); + const signature = await actualSigner.sign(toBeSigned); + const forged: RevocationEntry = [protectedHeader, {}, payload, signature]; + + const verdict = await verifyRevocationEntry(forged, { + identity: actualSigner, + }); + + expect(verdict).toEqual({ ok: false, reason: "wrong_issuer" }); + }); + + it("rejects an entry whose payload doesn't parse as revocation-claims", async () => { + const payload = encodeBuf({ nonsense: true }); + const protectedHeader = encodeBuf({}); + const toBeSigned = encodeBuf([ + "Signature1", + protectedHeader, + new Uint8Array(0), + payload, + ]); + const signature = await issuer.sign(toBeSigned); + const malformed: RevocationEntry = [ + protectedHeader, + {}, + payload, + signature, + ]; + + const verdict = await verifyRevocationEntry(malformed, { + identity: issuer, + }); + + expect(verdict).toEqual({ ok: false, reason: "malformed" }); + }); + + it("ignores bearer identity: an entry is about the issuer's token, not who presented the frame", async () => { + const entry = await signRevocationEntry(issuer, nextTokenId()); + const anyOtherViewer = await generateEs256Identity(); + + const verdict = await verifyRevocationEntry(entry, { + identity: anyOtherViewer, + }); + + // The identity port supplies only crypto primitives (verify/derive); verification succeeds regardless of which port instance performs it. + expect(verdict.ok).toBe(true); + }); }); describe("createMemoryStorage / createSystemClock", () => { From decf182b1a27607bffc8dbef68c1a2d665e0afbe Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 13:31:01 +0100 Subject: [PATCH 09/18] docs: describe issuer-matched revocation and the dropped federation family --- ts/packages/core/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/packages/core/README.md b/ts/packages/core/README.md index f25f1fc..4c7b7c2 100644 --- a/ts/packages/core/README.md +++ b/ts/packages/core/README.md @@ -12,11 +12,11 @@ The TypeScript implementation of wire-mesh's protocol, built ports/adapters: dom ## Domain logic implemented - **Handshake negotiation** (`src/domain/handshake.ts`) -- protocol-version and capability-domain negotiation between two peers, the mechanism agent-comms issue #31 is fixed by. -- **Capability-token verification** (`src/domain/tokens.ts`) -- the full chain tokens.cddl documents: COSE_Sign1 signature verification, the self-certifying issuer-key check (`sha256(issuer-key.public-key) == issuer`), expiry/not-before, revocation, and recursive delegation-chain narrowing (a delegated token's issuer must be its parent's bearer, and its expiry must not exceed its parent's). +- **Capability-token verification** (`src/domain/tokens.ts`) -- the full chain tokens.cddl documents: COSE_Sign1 signature verification, the self-certifying issuer-key check (`sha256(issuer-key.public-key) == issuer`), expiry/not-before, issuer-matched revocation (only a token's own issuer's signed revocation-entry counts, checked across every ancestor in the delegation chain, not just the leaf), and recursive delegation-chain narrowing (a delegated token's issuer must be its parent's bearer, and its expiry must not exceed its parent's). Also exports `verifyRevocationEntry` for ingesting gossiped revocation-announce frames: each entry is itself a signed, self-certifying COSE_Sign1 over revocation-claims, verified before it may enter the revocation view. ## Deliberately deferred -Every other frame family (management/exec, streaming, data-domain, federation) is covered by schema validation only -- `conformance-check` proves the generated schemas decode and re-encode every golden vector byte-exactly, including these families, but no domain-level business logic (dispatch, session bookkeeping, PTY/proc lifecycle, oplog replication, federation-link state) exists for them yet. This matches the build-out plan's own stated option to land "transport + handshake + tokens first... with streaming/exec/federation behind later milestones" -- the conformance suite covers all families either way, so nothing here is unverified, only unimplemented. +Every other frame family (management/exec, streaming, data-domain, discovery, coordinator election) is covered by schema validation only -- `conformance-check` proves the generated schemas decode and re-encode every golden vector byte-exactly, including these families, but no domain-level business logic (dispatch, session bookkeeping, PTY/proc lifecycle, oplog replication, coordinator term tracking) exists for them yet. This matches the build-out plan's own stated option to land "transport + handshake + tokens first" with the rest behind later milestones -- the conformance suite covers all families either way, so nothing here is unverified, only unimplemented. (The federation link protocol no longer exists in the spec at all: cross-scope sharing is ordinary capability-token delegation, and `src/domain/tokens.ts`'s verification chain is exactly the mechanism that governs it.) ## Regenerating the schema From 17cb5a41ed3b0f0eadd58079230fbee1d7a7c527 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 13:32:05 +0100 Subject: [PATCH 10/18] chore: refresh the cddl.js lockfile resolution to current main --- ts/pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index ec87664..a07dab3 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 2.3.0 cddl.js: specifier: github:ExaDev/cddl.js#main - version: https://codeload.github.com/ExaDev/cddl.js/tar.gz/fbaed5ff48d47c856f554b3f3e1f37aac98e0d80 + version: https://codeload.github.com/ExaDev/cddl.js/tar.gz/8de0f0cf0e8eb14e347894a55bdb655d5f332bc5 zod: specifier: 4.5.4 version: 4.5.4 @@ -625,8 +625,8 @@ packages: resolution: {integrity: sha512-76WB3hq8BoaGkMkBVJ27fW5LJU+qqDLEpgRNCG/SYKhODWXpVPOTD4UcUto3IEzYLA52nsvbhb0wabhHDn3qXg==} engines: {node: '>=20'} - cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/fbaed5ff48d47c856f554b3f3e1f37aac98e0d80: - resolution: {tarball: https://codeload.github.com/ExaDev/cddl.js/tar.gz/fbaed5ff48d47c856f554b3f3e1f37aac98e0d80} + cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/8de0f0cf0e8eb14e347894a55bdb655d5f332bc5: + resolution: {tarball: https://codeload.github.com/ExaDev/cddl.js/tar.gz/8de0f0cf0e8eb14e347894a55bdb655d5f332bc5} version: 0.0.0 chai@6.2.2: @@ -1882,7 +1882,7 @@ snapshots: dependencies: '@cto.af/wtf8': 0.0.5 - cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/fbaed5ff48d47c856f554b3f3e1f37aac98e0d80: + cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/8de0f0cf0e8eb14e347894a55bdb655d5f332bc5: dependencies: camelcase: 9.0.0 cbor2: 2.3.0 From d0497d11169bf3ef63659764aacc02b1321d087c Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 14:04:03 +0100 Subject: [PATCH 11/18] fix: delegation must narrow scope and capability, not just expiry Three verifier defects from review, all in the delegation chain: Scope narrowing was not implemented at all -- only the bearer-chain and expiry checks existed, so a child token with kind:"org" over /everything, a sibling path (/home/private under /work), or no path at all (the kind's whole-scope root) all verified under a path-narrowed parent. Narrowing now covers all three axes of authority: identical kind, an equal-or-descendant path compared on "/"-segment boundaries (/work/sub narrows /work; /workbook does not), and an identical capability verb (the verb grammar has no sub-verb relation, so a different verb is a different authority, not a narrower one). expectedBearer was propagated into the parent recursion, failing every ancestor in any chain of two or more hops -- the parent's bearer is structurally the child's issuer, never the leaf's presenter. It now applies to the leaf verdict only. decode() on a hostile payload or parent field threw instead of returning a verdict; both are now caught and mapped to the malformed/parent_invalid reasons that already existed for exactly this. --- ts/packages/core/src/domain/tokens.ts | 77 +++++-- ts/packages/core/test/tokens.test.ts | 281 ++++++++++++++++++++++++++ 2 files changed, 344 insertions(+), 14 deletions(-) diff --git a/ts/packages/core/src/domain/tokens.ts b/ts/packages/core/src/domain/tokens.ts index 7b9c2f7..3bea604 100644 --- a/ts/packages/core/src/domain/tokens.ts +++ b/ts/packages/core/src/domain/tokens.ts @@ -49,6 +49,27 @@ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { return true; } +/** True when childPath is parentPath or a descendant of it, compared on "/"-segment boundaries: "/work/sub" narrows "/work", but "/workbook" does NOT narrow "/work" despite the string prefix, because "book" continues the same segment. */ +function pathNarrows(childPath: string, parentPath: string): boolean { + if (childPath === parentPath) return true; + if (!childPath.startsWith(parentPath)) return false; + if (parentPath.endsWith("/")) return true; + return childPath.charAt(parentPath.length) === "/"; +} + +/** + * True when childScope narrows parentScope per tokens.cddl ("each hop can only narrow authority, never widen it"): the kind must be identical (a different kind is a different kind of authority, not a narrower one), and a parent with a path requires the child to carry an equal-or-descendant path -- an absent child path means the kind's whole-scope root, which is wider than any path-narrowed parent. A parent with no path (whole-scope root) lets any child path under the same kind through. + */ +function scopeNarrows( + parent: TokenClaims["scope"], + child: TokenClaims["scope"], +): boolean { + if (parent.kind !== child.kind) return false; + if (parent.path === undefined) return true; + if (child.path === undefined) return false; + return pathNarrows(child.path, parent.path); +} + /** RFC 9052 §4.4 Sig_structure for a COSE_Sign1 with no external AAD: ["Signature1", protected, external_aad, payload]. */ function sig1ToBeSigned( protectedHeader: Uint8Array, @@ -61,18 +82,45 @@ function sig1ToBeSigned( } /** - * Verifies one capability token per tokens.cddl's own documented rules: the token is a well-formed COSE_Sign1 whose signature actually verifies against its own embedded issuer-key, that issuer-key is self-certifying (sha256(issuer-key.public-key) equals the claimed issuer device-id -- no shared secret needed to check this), the token is currently valid (not expired, not before not-before, not revoked), and -- recursively -- any parent delegation narrows rather than widens: the parent's bearer must be this token's issuer (the delegation chain is unbroken), and this token's expiry must not exceed its parent's. + * Verifies one capability token per tokens.cddl's own documented rules: the token is a well-formed COSE_Sign1 whose signature actually verifies against its own embedded issuer-key, that issuer-key is self-certifying (sha256(issuer-key.public-key) equals the claimed issuer device-id -- no shared secret needed to check this), the token is currently valid (not expired, not before not-before, not revoked by its own issuer), and -- recursively -- any parent delegation narrows rather than widens across all three axes of authority: the parent's bearer must be this token's issuer (the delegation chain is unbroken), this token's expiry must not exceed its parent's, and this token's scope must narrow its parent's (same kind; equal-or-descendant path when the parent carries one) with an identical capability verb (the capability-verb grammar has no sub-verb relation, so a different verb is a different authority, not a narrower one). Undecodable payload bytes return "malformed" and undecodable parent bytes return "parent_invalid" -- hostile input produces a verdict, never a throw. */ export async function verifyCapabilityToken( token: CapabilityToken, options: VerifyCapabilityTokenOptions, +): Promise { + // expectedBearer applies to the leaf only: in any valid chain the parent's bearer is the child's issuer (structurally enforced below), never the leaf's presenter, so consulting it during the recursive walk would wrongly fail every ancestor. + const verdict = await verifyTokenChain(token, { + identity: options.identity, + clock: options.clock, + revocation: options.revocation, + }); + if (!verdict.ok) { + return verdict; + } + if ( + options.expectedBearer !== undefined && + !bytesEqual(verdict.claims.bearer, options.expectedBearer) + ) { + return { ok: false, reason: "bearer_mismatch" }; + } + return verdict; +} + +async function verifyTokenChain( + token: CapabilityToken, + options: Omit, ): Promise { const [protectedHeader, , payload, signature] = token; if (payload === null) { return { ok: false, reason: "malformed" }; } - const decodedClaims: unknown = decode(payload, cdeDecodeOptions); + let decodedClaims: unknown; + try { + decodedClaims = decode(payload, cdeDecodeOptions); + } catch { + return { ok: false, reason: "malformed" }; + } const claimsResult = tokenClaimsSchema.safeParse(decodedClaims); if (!claimsResult.success) { return { ok: false, reason: "malformed" }; @@ -108,15 +156,17 @@ export async function verifyCapabilityToken( } if (claims.parent !== undefined) { - const decodedParent: unknown = decode(claims.parent, cdeDecodeOptions); + let decodedParent: unknown; + try { + decodedParent = decode(claims.parent, cdeDecodeOptions); + } catch { + return { ok: false, reason: "parent_invalid" }; + } const parentResult = capabilityTokenSchema.safeParse(decodedParent); if (!parentResult.success) { return { ok: false, reason: "parent_invalid" }; } - const parentVerdict = await verifyCapabilityToken( - parentResult.data, - options, - ); + const parentVerdict = await verifyTokenChain(parentResult.data, options); if (!parentVerdict.ok) { return { ok: false, reason: "parent_invalid" }; } @@ -126,13 +176,12 @@ export async function verifyCapabilityToken( if (claims.expires > parentVerdict.claims.expires) { return { ok: false, reason: "delegation_exceeds_parent" }; } - } - - if ( - options.expectedBearer !== undefined && - !bytesEqual(claims.bearer, options.expectedBearer) - ) { - return { ok: false, reason: "bearer_mismatch" }; + if (!scopeNarrows(parentVerdict.claims.scope, claims.scope)) { + return { ok: false, reason: "delegation_exceeds_parent" }; + } + if (parentVerdict.claims.capability !== claims.capability) { + return { ok: false, reason: "delegation_exceeds_parent" }; + } } return { ok: true, claims }; diff --git a/ts/packages/core/test/tokens.test.ts b/ts/packages/core/test/tokens.test.ts index 51e3ec8..d6a3ab1 100644 --- a/ts/packages/core/test/tokens.test.ts +++ b/ts/packages/core/test/tokens.test.ts @@ -418,6 +418,287 @@ describe("verifyCapabilityToken", () => { expect(verdict).toEqual({ ok: false, reason: "parent_invalid" }); }); + + // -- Delegation must narrow scope and capability, not just expiry -- + + interface DelegatedSeed { + tokenId: Uint8Array; + bearer: DeviceId; + scope: CapabilityScope; + capability?: string; + expires: number; + parent: CapabilityToken; + } + + /** Signs a child token as `identity` with an explicit parent token embedded -- the parent's own claims are recoverable by the verifier's own recursion, so they are not restated here. */ + async function signDelegated( + identity: IdentityPort, + seed: DelegatedSeed, + ): Promise { + const claims: TokenClaims = { + "token-id": seed.tokenId, + issuer: identity.deviceId, + "issuer-key": identity.identityKey, + bearer: seed.bearer, + capability: seed.capability ?? "exec:pty", + scope: seed.scope, + expires: seed.expires, + parent: encodeBuf(seed.parent), + }; + const payload = encodeBuf(claims); + const protectedHeader = encodeBuf({}); + const toBeSigned = encodeBuf([ + "Signature1", + protectedHeader, + new Uint8Array(0), + payload, + ]); + const signature = await identity.sign(toBeSigned); + return [protectedHeader, {}, payload, signature]; + } + + /** A root /work-folder token plus a helper to delegate under it, signed by the root's bearer (bearerIdentity), keeping the common expiry/scope consistent across the narrowing tests. */ + async function delegateUnderWorkRoot( + childScope: Readonly, + childCapability?: string, + ): Promise { + const root = await signToken(issuer, { + tokenId: nextTokenId(), + bearer: bearerDeviceId, + scope: { kind: "folder", path: "/work" }, + expires: now + 2 * HOUR_MS, + }); + const delegate = await generateEs256Identity(); + return signDelegated(bearerIdentity, { + tokenId: nextTokenId(), + bearer: delegate.deviceId, + scope: childScope, + ...(childCapability !== undefined ? { capability: childCapability } : {}), + expires: now + HOUR_MS, + parent: root, + }); + } + + it("accepts a delegated token with the same scope as its parent", async () => { + const delegated = await delegateUnderWorkRoot({ + kind: "folder", + path: "/work", + }); + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict.ok).toBe(true); + }); + + it("accepts a delegated token whose path descends from the parent's", async () => { + const delegated = await delegateUnderWorkRoot({ + kind: "folder", + path: "/work/subdir/deeper", + }); + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict.ok).toBe(true); + }); + + it("rejects a delegated token whose scope kind differs from the parent's", async () => { + // kind:"org" is a different kind of authority, not a narrower one -- even though its path textually starts with /work + const delegated = await delegateUnderWorkRoot({ + kind: "org", + path: "/work", + }); + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ + ok: false, + reason: "delegation_exceeds_parent", + }); + }); + + it("rejects a delegated token whose path is a sibling, not a descendant", async () => { + const delegated = await delegateUnderWorkRoot({ + kind: "folder", + path: "/home/private", + }); + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ + ok: false, + reason: "delegation_exceeds_parent", + }); + }); + + it("rejects a delegated token whose path merely prefixes the parent's without a segment boundary", async () => { + // "/workbook" starts with "/work" as a string but is a different path, not a descendant + const delegated = await delegateUnderWorkRoot({ + kind: "folder", + path: "/workbook", + }); + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ + ok: false, + reason: "delegation_exceeds_parent", + }); + }); + + it("rejects a delegated token with no path under a path-narrowed parent", async () => { + // Absent path means the kind's whole-scope root, which is wider than the parent's /work + const delegated = await delegateUnderWorkRoot({ kind: "folder" }); + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ + ok: false, + reason: "delegation_exceeds_parent", + }); + }); + + it("rejects a delegated token whose capability verb differs from the parent's", async () => { + const delegated = await delegateUnderWorkRoot( + { kind: "folder", path: "/work" }, + "exec:proc", + ); + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ + ok: false, + reason: "delegation_exceeds_parent", + }); + }); + + it("applies expectedBearer to the leaf only, never to ancestors in the chain", async () => { + // The parent's bearer is the child's issuer (bearerIdentity), NOT the leaf's presenter: a leaf presented by its own delegate must verify even though the ancestor's bearer differs. + const root = await signToken(issuer, { + tokenId: nextTokenId(), + bearer: bearerDeviceId, + scope: { kind: "folder", path: "/work" }, + expires: now + 2 * HOUR_MS, + }); + const delegate = await generateEs256Identity(); + const delegated = await signDelegated(bearerIdentity, { + tokenId: nextTokenId(), + bearer: delegate.deviceId, + scope: { kind: "folder", path: "/work" }, + expires: now + HOUR_MS, + parent: root, + }); + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + expectedBearer: delegate.deviceId, + }); + + expect(verdict.ok).toBe(true); + }); + + // -- Hostile input must produce verdicts, not throws -- + + it("returns malformed for a payload whose bytes are not CBOR at all", async () => { + // A wrapped CBOR break byte: decodes to a bstr rather than token-claims, so this exercises schema rejection of a decodable-but-wrong payload + const garbage = encodeBuf(buf(Buffer.from("ff", "hex"))); + const hostile: CapabilityToken = [ + encodeBuf({}), + {}, + garbage, + new Uint8Array(P256_SIGNATURE_BYTE_LENGTH), + ]; + + const verdict = await verifyCapabilityToken(hostile, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ ok: false, reason: "malformed" }); + }); + + it("returns malformed for a payload whose CBOR map keys are not canonically ordered", async () => { + // CDE requires canonical (length-first) map-key ordering; cbor2's cdeDecodeOptions rejects this encoding + const reversedKeys = buf(Buffer.from("a2627a7a01616102", "hex")); + const hostile: CapabilityToken = [ + encodeBuf({}), + {}, + reversedKeys, + new Uint8Array(P256_SIGNATURE_BYTE_LENGTH), + ]; + + const verdict = await verifyCapabilityToken(hostile, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ ok: false, reason: "malformed" }); + }); + + it("returns parent_invalid for a parent whose bytes are not CBOR", async () => { + // Raw, unwrapped 0xff (a top-level CBOR BREAK -- decode throws): wrapped via encode() it would become a well-formed bstr and exercise schema rejection instead of the decode-throw path this test exists for + const garbageParent = buf(new Uint8Array([LOW_BYTE_MASK])); + // Signed with real claims but a garbage parent field, so the token itself is otherwise well-formed and the failure is isolated to parent decoding + const claims: TokenClaims = { + "token-id": nextTokenId(), + issuer: bearerDeviceId, + "issuer-key": bearerIdentity.identityKey, + bearer: (await generateEs256Identity()).deviceId, + capability: "exec:pty", + scope: { kind: "folder", path: "/work" }, + expires: now + HOUR_MS, + parent: garbageParent, + }; + const payload = encodeBuf(claims); + const protectedHeader = encodeBuf({}); + const toBeSigned = encodeBuf([ + "Signature1", + protectedHeader, + new Uint8Array(0), + payload, + ]); + const signature = await bearerIdentity.sign(toBeSigned); + const hostile: CapabilityToken = [protectedHeader, {}, payload, signature]; + + const verdict = await verifyCapabilityToken(hostile, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ ok: false, reason: "parent_invalid" }); + }); }); describe("verifyRevocationEntry", () => { From 24a74e3accbabfecfc24a49e300a4d1e53ba34ee Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 14:04:17 +0100 Subject: [PATCH 12/18] fix: never negotiate a retired capability domain core/federation is retired per handshake.cddl ("a peer must never advertise or negotiate it"), but negotiate() intersected advertised domains unfiltered, so two peers that buggily advertise it would end up speaking a dead domain. Retired domains are now excluded from the intersection even when both sides advertise them. --- ts/packages/core/src/domain/handshake.ts | 12 +++++++++--- ts/packages/core/test/handshake.test.ts | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/ts/packages/core/src/domain/handshake.ts b/ts/packages/core/src/domain/handshake.ts index 28e5793..072e459 100644 --- a/ts/packages/core/src/domain/handshake.ts +++ b/ts/packages/core/src/domain/handshake.ts @@ -7,11 +7,16 @@ import type { /** The highest protocol version this build of core understands. */ export const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion = 1; +/** + * Domain names that must never be negotiated, per handshake.cddl: core/federation is retired (its string stays reserved, never reused) and "a peer must never advertise or negotiate it". Excluded here even if both peers advertise it -- two buggy advertisers must not end up speaking a dead domain. + */ +const RETIRED_DOMAINS: readonly DomainId[] = ["core/federation"]; + export interface NegotiationResult { ok: boolean; /** The version both peers will speak for the rest of the session -- the lower of the two offered versions, so a peer never has to understand a frame shape it didn't advertise. */ version: ProtocolVersion; - /** Domains both peers advertised -- the only ones either side may address for the rest of the session. */ + /** Domains both peers advertised and that are not retired -- the only ones either side may address for the rest of the session. */ sharedDomains: DomainId[]; } @@ -23,8 +28,9 @@ export function negotiate( remote: HandshakeFrame, ): NegotiationResult { const version = Math.min(local.version, remote.version); - const sharedDomains = local.domains.filter((domain) => - remote.domains.includes(domain), + const sharedDomains = local.domains.filter( + (domain) => + !RETIRED_DOMAINS.includes(domain) && remote.domains.includes(domain), ); return { ok: version >= 1 && sharedDomains.length > 0, diff --git a/ts/packages/core/test/handshake.test.ts b/ts/packages/core/test/handshake.test.ts index d46ae74..127b12a 100644 --- a/ts/packages/core/test/handshake.test.ts +++ b/ts/packages/core/test/handshake.test.ts @@ -35,6 +35,25 @@ describe("negotiate", () => { expect(result.sharedDomains).toEqual([]); }); + it("never negotiates a retired domain, even when both peers advertise it", () => { + // core/federation is retired (handshake.cddl): a peer must never advertise or negotiate it. Two buggy peers both advertising it must still not end up speaking it. + const result = negotiate( + handshake(1, ["core/management", "core/federation"]), + handshake(1, ["core/federation", "core/management"]), + ); + expect(result.sharedDomains).toEqual(["core/management"]); + expect(result.ok).toBe(true); + }); + + it("fails when the only shared domain is a retired one", () => { + const result = negotiate( + handshake(1, ["core/federation"]), + handshake(1, ["core/federation"]), + ); + expect(result.sharedDomains).toEqual([]); + expect(result.ok).toBe(false); + }); + it("succeeds when versions and domains both overlap", () => { const result = negotiate( handshake(1, ["core/management"]), From 35203fc82575a724c47f062be1492041a55bc370 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 14:04:17 +0100 Subject: [PATCH 13/18] fix: undecodable frames reject the connection instead of crashing the process frameReader called cbor2 decode() unguarded from the socket data handler, so one malformed length-prefixed body threw inside the EventEmitter listener and killed the process -- any peer could DoS any node. A body that fails to decode now rejects the receive() iteration and destroys that one connection: hostile wire input is a connection-level failure surfaced through the Transport port, not a process crash and not a silently swallowed frame. Schema-invalid but decodable frames are still dropped in-stream, as before -- an unrecognised frame from a newer peer is what version negotiation exists to tolerate. --- .../core/src/adapters/tcp-transport.ts | 44 +++++++++++-- ts/packages/core/test/tcp-transport.test.ts | 66 +++++++++++++++++++ 2 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 ts/packages/core/test/tcp-transport.test.ts diff --git a/ts/packages/core/src/adapters/tcp-transport.ts b/ts/packages/core/src/adapters/tcp-transport.ts index b2e16c5..bc2f170 100644 --- a/ts/packages/core/src/adapters/tcp-transport.ts +++ b/ts/packages/core/src/adapters/tcp-transport.ts @@ -24,12 +24,16 @@ function writeFrame(socket: Socket, frame: Frame): void { socket.write(body); } -/** Reassembles length-prefixed CBOR frames from a byte stream, validating each against frameSchema before handing it to a consumer. */ +/** Reassembles length-prefixed CBOR frames from a byte stream, validating each against frameSchema before handing it to a consumer. A body that doesn't even decode as CBOR rejects the receive() iteration and destroys the connection -- hostile wire input is a connection-level failure, surfaced through the Transport port rather than crashing the process or being silently swallowed. */ function frameReader(socket: Socket): AsyncIterable { let buffer = Buffer.alloc(0); const pending: Frame[] = []; - const waiters: ((value: IteratorResult) => void)[] = []; + const waiters: { + resolve: (result: IteratorResult) => void; + reject: (error: unknown) => void; + }[] = []; let ended = false; + let failure: Error | null = null; function tryDrain(): void { while (buffer.length >= LENGTH_PREFIX_BYTES) { @@ -41,12 +45,24 @@ function frameReader(socket: Socket): AsyncIterable { ); buffer = buffer.subarray(LENGTH_PREFIX_BYTES + bodyLength); - const decoded: unknown = decode(body, cdeDecodeOptions); + let decoded: unknown; + try { + decoded = decode(body, cdeDecodeOptions); + } catch (error) { + // socket.destroy takes an Error; the caught value is unknown-typed even though cbor2 only ever throws Errors, so it is rewrapped rather than asserted + const connectionError = + error instanceof Error + ? error + : new Error(`frame body failed to decode: ${String(error)}`); + failAll(connectionError); + socket.destroy(connectionError); + return; + } const result = frameSchema.safeParse(decoded); if (result.success) { const waiter = waiters.shift(); if (waiter) { - waiter({ value: result.data, done: false }); + waiter.resolve({ value: result.data, done: false }); } else { pending.push(result.data); } @@ -58,7 +74,16 @@ function frameReader(socket: Socket): AsyncIterable { function endAll(): void { ended = true; for (const waiter of waiters.splice(0)) { - waiter({ value: undefined, done: true }); + waiter.resolve({ value: undefined, done: true }); + } + } + + /** Ends the iteration with the connection-level error: pending and future next() calls reject, so a consumer iterating receive() sees the failure where it consumed the stream. */ + function failAll(error: Error): void { + failure = error; + ended = true; + for (const waiter of waiters.splice(0)) { + waiter.reject(error); } } @@ -68,6 +93,8 @@ function frameReader(socket: Socket): AsyncIterable { }); socket.on("end", endAll); socket.on("close", endAll); + // destroy(error) above re-emits the failure as a socket 'error' event; it's already been delivered to the consumer through the rejected iteration, so this listener exists to stop EventEmitter treating it as a second, unhandled crash + socket.on("error", () => undefined); return { [Symbol.asyncIterator]() { @@ -77,11 +104,14 @@ function frameReader(socket: Socket): AsyncIterable { if (next !== undefined) { return Promise.resolve({ value: next, done: false }); } + if (failure !== null) { + return Promise.reject(failure); + } if (ended) { return Promise.resolve({ value: undefined, done: true }); } - return new Promise((resolve) => { - waiters.push(resolve); + return new Promise((resolve, reject) => { + waiters.push({ resolve, reject }); }); }, }; diff --git a/ts/packages/core/test/tcp-transport.test.ts b/ts/packages/core/test/tcp-transport.test.ts new file mode 100644 index 0000000..1ba2b61 --- /dev/null +++ b/ts/packages/core/test/tcp-transport.test.ts @@ -0,0 +1,66 @@ +import { connect as netConnect } from "node:net"; +import { describe, expect, it } from "vitest"; +import { createTcpTransport } from "../src/adapters/tcp-transport.js"; + +const LENGTH_PREFIX_BYTES = 4; +// A fixed address rather than port 0: the Transport port's listen takes the caller's address and never reports the bound one, so an OS-assigned port would be undiscoverable through the public surface this test deliberately exercises. +const TEST_LISTEN_ADDRESS = "127.0.0.1:44833"; +const TEST_LISTEN_PORT = Number(TEST_LISTEN_ADDRESS.split(":")[1]); + +/** Connects a raw socket (deliberately NOT the transport's own Connection -- the point is to write hostile bytes a well-behaved peer would never produce) and writes one length-prefixed body. */ +async function writeRawBody(port: number, body: Uint8Array): Promise { + return new Promise((resolve, reject) => { + const socket = netConnect({ host: "127.0.0.1", port }); + socket.once("connect", () => { + const header = Buffer.alloc(LENGTH_PREFIX_BYTES); + header.writeUInt32BE(body.length, 0); + socket.write(header); + socket.write(body, () => { + socket.end(); + resolve(); + }); + }); + socket.once("error", reject); + }); +} + +describe("createTcpTransport", () => { + it("surfaces an undecodable frame as a connection-level error, not a process crash", async () => { + const transport = createTcpTransport(); + let resolveError: (value: unknown) => void; + const receivedError = new Promise((resolve) => { + resolveError = resolve; + }); + const stopListening = await transport.listen( + TEST_LISTEN_ADDRESS, + (connection) => { + void (async () => { + const frames: unknown[] = []; + for await (const frame of connection.receive()) { + frames.push(frame); + } + return frames; + })().then( + () => { + // A non-Error marker: the assertion below distinguishes a genuine rejection from clean completion + resolveError("COMPLETED"); + }, + (error: unknown) => { + resolveError(error); + }, + ); + }, + ); + + // A lone top-level CBOR BREAK byte: decode() throws on it inside the socket data handler -- previously an uncaughtException that killed the process + await writeRawBody( + TEST_LISTEN_PORT, + Uint8Array.from(Buffer.from("ff", "hex")), + ); + + const error = await receivedError; + expect(error).toBeInstanceOf(Error); + + await stopListening(); + }); +}); From da847b89d4d71f416f33ff5b4125e14b7a4d92a0 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 14:04:32 +0100 Subject: [PATCH 14/18] fix: stop caching _generate on a key that ignores the spec The task's real input (../../../spec/protocol.cddl) lives above this pnpm workspace's root, and turbo refuses workspace-external paths in both task inputs and globalDependencies -- so every cache hit was keyed on generate.ts alone and replayed stale output after a spec edit (reproduced: touch the spec, turbo replays the cached run). Generation is a sub-second script, so the _generate task is now simply uncached. --- ts/packages/core/turbo.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ts/packages/core/turbo.json b/ts/packages/core/turbo.json index dbad2d5..72e964e 100644 --- a/ts/packages/core/turbo.json +++ b/ts/packages/core/turbo.json @@ -9,8 +9,9 @@ }, "_generate": { "dependsOn": ["_build"], - "inputs": ["generate.ts"], - "outputs": ["src/generated/protocol.ts"] + // Uncached, deliberately: the task's real input (../../../spec/protocol.cddl) lives above this pnpm workspace's root, and turbo refuses to hash workspace-external paths in both task inputs and globalDependencies -- so any cache hit here is keyed on generate.ts alone and replays stale output after a spec edit. Generation is a sub-second script; caching it buys nothing and the staleness is real. + "cache": false, + "inputs": ["generate.ts"] }, "_test": { "dependsOn": ["_build"] From ef5619de4fc3e2762eacd1173de258b5052fd155 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 14:04:33 +0100 Subject: [PATCH 15/18] test: pin device-id derivation to sha256 of the raw public-key bytes Only exercised indirectly until now. The derivation is the exact identity.cddl rule both Cascade and agent-comms previously got wrong (hashing the whole certificate DER), so an explicit test recomputing sha256 over the same raw bytes catches any drift back toward hashing anything else. --- ts/packages/core/test/node-identity.test.ts | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 ts/packages/core/test/node-identity.test.ts diff --git a/ts/packages/core/test/node-identity.test.ts b/ts/packages/core/test/node-identity.test.ts new file mode 100644 index 0000000..b965f45 --- /dev/null +++ b/ts/packages/core/test/node-identity.test.ts @@ -0,0 +1,24 @@ +import { createHash, webcrypto } from "node:crypto"; +import { describe, expect, it } from "vitest"; +import { deriveDeviceId } from "../src/adapters/node-identity.js"; + +describe("deriveDeviceId", () => { + it("is exactly sha256 of the raw public-key bytes, not of any certificate or DER wrapping", async () => { + // Pins the derivation identity.cddl exists to guarantee: device-id = sha256(identity-key.public-key). Both Cascade and agent-comms previously shipped the whole-cert-DER fingerprint bug this rule exists to prevent -- a test that recomputes sha256 over the same raw bytes catches any drift back toward hashing anything else. + const keyPair = await webcrypto.subtle.generateKey( + { name: "ECDSA", namedCurve: "P-256" }, + true, + ["sign", "verify"], + ); + const publicKeyBytes = new Uint8Array( + await webcrypto.subtle.exportKey("raw", keyPair.publicKey), + ); + const expected = Uint8Array.from( + createHash("sha256").update(publicKeyBytes).digest(), + ); + + const deviceId = await deriveDeviceId(publicKeyBytes); + + expect(deviceId).toEqual(expected); + }); +}); From 8bfbb9bb52f18578d2755fc6810525c31dd2a87b Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 14:04:33 +0100 Subject: [PATCH 16/18] docs: describe scope narrowing in the README; correct the CI job name and the deferral rationale The token-verification bullet defined delegation narrowing as bearer-chain and expiry only, silently dropping the scope and capability axes the verifier enforces. The deferral paragraph cited a build-out plan that lives outside this repository; replaced with the actual rationale (first pass scopes domain logic to the families with real business rules; conformance already covers every family's wire shape). generate.ts's comment named a nonexistent core-verify CI job (it is ts-core-verify). --- ts/packages/core/README.md | 4 ++-- ts/packages/core/generate.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ts/packages/core/README.md b/ts/packages/core/README.md index 4c7b7c2..bd8cbf5 100644 --- a/ts/packages/core/README.md +++ b/ts/packages/core/README.md @@ -12,11 +12,11 @@ The TypeScript implementation of wire-mesh's protocol, built ports/adapters: dom ## Domain logic implemented - **Handshake negotiation** (`src/domain/handshake.ts`) -- protocol-version and capability-domain negotiation between two peers, the mechanism agent-comms issue #31 is fixed by. -- **Capability-token verification** (`src/domain/tokens.ts`) -- the full chain tokens.cddl documents: COSE_Sign1 signature verification, the self-certifying issuer-key check (`sha256(issuer-key.public-key) == issuer`), expiry/not-before, issuer-matched revocation (only a token's own issuer's signed revocation-entry counts, checked across every ancestor in the delegation chain, not just the leaf), and recursive delegation-chain narrowing (a delegated token's issuer must be its parent's bearer, and its expiry must not exceed its parent's). Also exports `verifyRevocationEntry` for ingesting gossiped revocation-announce frames: each entry is itself a signed, self-certifying COSE_Sign1 over revocation-claims, verified before it may enter the revocation view. +- **Capability-token verification** (`src/domain/tokens.ts`) -- the full chain tokens.cddl documents: COSE_Sign1 signature verification, the self-certifying issuer-key check (`sha256(issuer-key.public-key) == issuer`), expiry/not-before, issuer-matched revocation (only a token's own issuer's signed revocation-entry counts, checked across every ancestor in the delegation chain, not just the leaf), and recursive delegation-chain narrowing across all three axes of authority: a delegated token's issuer must be its parent's bearer (the chain is unbroken), its expiry must not exceed its parent's, and its scope must narrow its parent's (identical kind, equal-or-descendant path when the parent carries one) with an identical capability verb (the verb grammar has no sub-verb relation, so a different verb is different authority, not narrower). Also exports `verifyRevocationEntry` for ingesting gossiped revocation-announce frames: each entry is itself a signed, self-certifying COSE_Sign1 over revocation-claims, verified before it may enter the revocation view. ## Deliberately deferred -Every other frame family (management/exec, streaming, data-domain, discovery, coordinator election) is covered by schema validation only -- `conformance-check` proves the generated schemas decode and re-encode every golden vector byte-exactly, including these families, but no domain-level business logic (dispatch, session bookkeeping, PTY/proc lifecycle, oplog replication, coordinator term tracking) exists for them yet. This matches the build-out plan's own stated option to land "transport + handshake + tokens first" with the rest behind later milestones -- the conformance suite covers all families either way, so nothing here is unverified, only unimplemented. (The federation link protocol no longer exists in the spec at all: cross-scope sharing is ordinary capability-token delegation, and `src/domain/tokens.ts`'s verification chain is exactly the mechanism that governs it.) +Every other frame family (management/exec, streaming, data-domain, discovery, coordinator election) is covered by schema validation only -- `conformance-check` proves the generated schemas decode and re-encode every golden vector byte-exactly, including these families, but no domain-level business logic (dispatch, session bookkeeping, PTY/proc lifecycle, oplog replication, coordinator term tracking) exists for them yet. This first pass deliberately scopes domain logic to transport + handshake + tokens (the families with real business rules worth pinning down before the others), because the conformance suite already covers every family's wire shape either way -- nothing here is unverified, only unimplemented. (The federation link protocol no longer exists in the spec at all: cross-scope sharing is ordinary capability-token delegation, and `src/domain/tokens.ts`'s verification chain is exactly the mechanism that governs it.) ## Regenerating the schema diff --git a/ts/packages/core/generate.ts b/ts/packages/core/generate.ts index f6f28b8..4ca0b0a 100644 --- a/ts/packages/core/generate.ts +++ b/ts/packages/core/generate.ts @@ -1,4 +1,4 @@ -// Produces src/generated/protocol.ts from ../../../spec/protocol.cddl via cddl.js. Run `pnpm generate` after the spec changes, then `pnpm test` to confirm generated schemas still round-trip conformance/'s golden vectors. CI regenerates and diffs against the committed file (see .github/workflows/ci.yml's core-verify job) so protocol.ts is never edited by hand. +// Produces src/generated/protocol.ts from ../../../spec/protocol.cddl via cddl.js. Run `pnpm generate` after the spec changes, then `pnpm test` to confirm generated schemas still round-trip conformance/'s golden vectors. CI regenerates and diffs against the committed file (see .github/workflows/ci.yml's ts-core-verify job) so protocol.ts is never edited by hand. import { writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; From b417008f625c313658e998f8a23f2ea36a85a1ac Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 14:19:50 +0100 Subject: [PATCH 17/18] fix: harden verification against hostile revocation entries and relative path segments verifyRevocationEntry called decode() unguarded on attacker-controlled gossiped bytes, so an entry with a garbage CBOR payload or non-canonical map keys threw instead of returning the malformed verdict that already existed for exactly this -- the earlier hostile-input guard covered the token path only. Guarded, with both shapes tested. pathNarrows compared paths purely lexically, so a delegate could sign "/work/../org" (or "/work/a/../b") and verify under a "/work" parent -- a path that normalises outside what the parent authorised. The spec is silent on path syntax, so the comparison now fails closed: any path containing a "." or ".." segment never narrows anything, documented in the function comment. Also pins the neighbouring edges the rule composes with: trailing-slash child under a non-slash parent accepted, a "/" parent admits every well-formed child path, empty and case-different child paths rejected. --- ts/packages/core/src/domain/tokens.ts | 17 ++- ts/packages/core/test/tokens.test.ts | 146 ++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 2 deletions(-) diff --git a/ts/packages/core/src/domain/tokens.ts b/ts/packages/core/src/domain/tokens.ts index 3bea604..d62e862 100644 --- a/ts/packages/core/src/domain/tokens.ts +++ b/ts/packages/core/src/domain/tokens.ts @@ -49,8 +49,16 @@ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { return true; } -/** True when childPath is parentPath or a descendant of it, compared on "/"-segment boundaries: "/work/sub" narrows "/work", but "/workbook" does NOT narrow "/work" despite the string prefix, because "book" continues the same segment. */ +/** True when the path contains a "." or ".." segment. Purely lexical prefix comparison would let "/work/../org" pass under "/work" -- a path that normalises outside the parent -- so any relative segment fails the narrowing comparison wholesale: fail-closed rather than reimplementing path normalisation, consistent with how empty, case-different, and non-boundary-prefixed paths already behave. */ +function hasRelativeSegment(path: string): boolean { + return path.split("/").some((segment) => segment === "." || segment === ".."); +} + +/** True when childPath is parentPath or a descendant of it, compared on "/"-segment boundaries: "/work/sub" narrows "/work", but "/workbook" does NOT narrow "/work" despite the string prefix, because "book" continues the same segment. Paths containing "." or ".." segments never narrow anything (see hasRelativeSegment). */ function pathNarrows(childPath: string, parentPath: string): boolean { + if (hasRelativeSegment(childPath) || hasRelativeSegment(parentPath)) { + return false; + } if (childPath === parentPath) return true; if (!childPath.startsWith(parentPath)) return false; if (parentPath.endsWith("/")) return true; @@ -211,7 +219,12 @@ export async function verifyRevocationEntry( return { ok: false, reason: "malformed" }; } - const decodedClaims: unknown = decode(payload, cdeDecodeOptions); + let decodedClaims: unknown; + try { + decodedClaims = decode(payload, cdeDecodeOptions); + } catch { + return { ok: false, reason: "malformed" }; + } const claimsResult = revocationClaimsSchema.safeParse(decodedClaims); if (!claimsResult.success) { return { ok: false, reason: "malformed" }; diff --git a/ts/packages/core/test/tokens.test.ts b/ts/packages/core/test/tokens.test.ts index d6a3ab1..1559872 100644 --- a/ts/packages/core/test/tokens.test.ts +++ b/ts/packages/core/test/tokens.test.ts @@ -565,6 +565,77 @@ describe("verifyCapabilityToken", () => { }); }); + it("rejects a delegated token whose path uses a .. segment to escape the parent's", async () => { + // Purely lexical prefix comparison would accept "/work/../org" under "/work"; a path that normalises outside the parent is a widening, so any "." or ".." segment fails the narrowing comparison + const delegated = await delegateUnderWorkRoot({ + kind: "folder", + path: "/work/../org", + }); + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ + ok: false, + reason: "delegation_exceeds_parent", + }); + }); + + it("rejects a delegated token with a nested .. segment even when it stays inside the parent", async () => { + // "/work/a/../b" normalises to "/work/b" which would narrow, but relative segments are rejected wholesale: fail-closed rather than reimplementing path normalisation + const delegated = await delegateUnderWorkRoot({ + kind: "folder", + path: "/work/a/../b", + }); + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ + ok: false, + reason: "delegation_exceeds_parent", + }); + }); + + it("accepts a delegated token whose child path carries a trailing slash under the parent", async () => { + const delegated = await delegateUnderWorkRoot({ + kind: "folder", + path: "/work/", + }); + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict.ok).toBe(true); + }); + + it("rejects a delegated token whose child path differs only in case", async () => { + const delegated = await delegateUnderWorkRoot({ + kind: "folder", + path: "/Work", + }); + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ + ok: false, + reason: "delegation_exceeds_parent", + }); + }); + it("rejects a delegated token with no path under a path-narrowed parent", async () => { // Absent path means the kind's whole-scope root, which is wider than the parent's /work const delegated = await delegateUnderWorkRoot({ kind: "folder" }); @@ -599,6 +670,52 @@ describe("verifyCapabilityToken", () => { }); }); + async function delegateUnderRootRoot( + childPath: string, + ): Promise { + const root = await signToken(issuer, { + tokenId: nextTokenId(), + bearer: bearerDeviceId, + scope: { kind: "folder", path: "/" }, + expires: now + 2 * HOUR_MS, + }); + const delegate = await generateEs256Identity(); + return signDelegated(bearerIdentity, { + tokenId: nextTokenId(), + bearer: delegate.deviceId, + scope: { kind: "folder", path: childPath }, + expires: now + HOUR_MS, + parent: root, + }); + } + + it("accepts any well-formed child path under a whole-root parent path", async () => { + const delegated = await delegateUnderRootRoot("/anything/at/all"); + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict.ok).toBe(true); + }); + + it("rejects an empty child path", async () => { + const delegated = await delegateUnderWorkRoot({ kind: "folder", path: "" }); + + const verdict = await verifyCapabilityToken(delegated, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + + expect(verdict).toEqual({ + ok: false, + reason: "delegation_exceeds_parent", + }); + }); + it("applies expectedBearer to the leaf only, never to ancestors in the chain", async () => { // The parent's bearer is the child's issuer (bearerIdentity), NOT the leaf's presenter: a leaf presented by its own delegate must verify even though the ancestor's bearer differs. const root = await signToken(issuer, { @@ -806,6 +923,35 @@ describe("verifyRevocationEntry", () => { // The identity port supplies only crypto primitives (verify/derive); verification succeeds regardless of which port instance performs it. expect(verdict.ok).toBe(true); }); + + it("returns malformed for an entry whose payload bytes are not CBOR, instead of throwing", async () => { + // This function's whole purpose is ingesting hostile gossiped entries: a lone top-level CBOR BREAK byte must produce the malformed verdict, never a throw + const garbage = buf(Buffer.from("ff", "hex")); + const hostile: RevocationEntry = [ + encodeBuf({}), + {}, + garbage, + new Uint8Array(P256_SIGNATURE_BYTE_LENGTH), + ]; + + const verdict = await verifyRevocationEntry(hostile, { identity: issuer }); + + expect(verdict).toEqual({ ok: false, reason: "malformed" }); + }); + + it("returns malformed for an entry whose payload map keys are not canonically ordered, instead of throwing", async () => { + const nonCanonical = buf(Buffer.from("a2627a7a01616102", "hex")); + const hostile: RevocationEntry = [ + encodeBuf({}), + {}, + nonCanonical, + new Uint8Array(P256_SIGNATURE_BYTE_LENGTH), + ]; + + const verdict = await verifyRevocationEntry(hostile, { identity: issuer }); + + expect(verdict).toEqual({ ok: false, reason: "malformed" }); + }); }); describe("createMemoryStorage / createSystemClock", () => { From 11a3973309b2afe80caf687151972ccbea5975e0 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Thu, 10 Sep 2026 14:20:02 +0100 Subject: [PATCH 18/18] feat: report the bound address from Transport.listen listen() resolved to a bare close function, so a caller passing port 0 could never learn the OS-assigned port -- the contract had no path from "I asked for any free port" to "here is the address to dial". It now resolves a Listener carrying both the close handle and the address actually bound. The TCP adapter reads it back from the server's own listening callback; the transport test binds port 0 instead of a fixed port, so concurrent CI runs can never collide. --- .../core/src/adapters/tcp-transport.ts | 18 +++++-- ts/packages/core/src/ports/transport.ts | 11 ++++- ts/packages/core/test/tcp-transport.test.ts | 49 ++++++++----------- 3 files changed, 43 insertions(+), 35 deletions(-) diff --git a/ts/packages/core/src/adapters/tcp-transport.ts b/ts/packages/core/src/adapters/tcp-transport.ts index bc2f170..18ee938 100644 --- a/ts/packages/core/src/adapters/tcp-transport.ts +++ b/ts/packages/core/src/adapters/tcp-transport.ts @@ -1,7 +1,7 @@ import { connect as netConnect, createServer, type Socket } from "node:net"; import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2"; import { frameSchema, type Frame } from "../generated/protocol.js"; -import type { Connection, Transport } from "../ports/transport.js"; +import type { Connection, Listener, Transport } from "../ports/transport.js"; const LENGTH_PREFIX_BYTES = 4; @@ -150,7 +150,7 @@ export function createTcpTransport(): Transport { socket.once("error", reject); }); }, - async listen(address, onConnection) { + async listen(address, onConnection): Promise { const { host, port } = parseAddress(address); return new Promise((resolve, reject) => { const server = createServer((socket) => { @@ -158,14 +158,22 @@ export function createTcpTransport(): Transport { }); server.once("error", reject); server.listen(port, host, () => { - resolve( - async () => + const bound = server.address(); + if (bound === null || typeof bound === "string") { + // A TCP server's bound address is always an AddressInfo object; null only if the server were not listening, which cannot hold inside this listening callback + reject(new Error("listener did not report a bound address")); + return; + } + const listener: Listener = { + close: async () => new Promise((resolveClose) => { server.close(() => { resolveClose(); }); }), - ); + address: `${bound.address}:${String(bound.port)}`, + }; + resolve(listener); }); }); }, diff --git a/ts/packages/core/src/ports/transport.ts b/ts/packages/core/src/ports/transport.ts index 94d3c69..dcf9bac 100644 --- a/ts/packages/core/src/ports/transport.ts +++ b/ts/packages/core/src/ports/transport.ts @@ -10,11 +10,18 @@ export interface Connection { close: () => Promise; } +export interface Listener { + /** Stops listening and closes the listener. */ + close: () => Promise; + /** The address actually bound, as "host:port". A caller may pass port 0 to take an OS-assigned port and needs it handed back to connect to (or advertise) -- tests rely on this to avoid fixed-port collisions. */ + address: string; +} + export interface Transport { connect: (address: string) => Promise; - /** Starts listening; each accepted connection is handed to onConnection. Returns a function that stops listening and closes the listener. */ + /** Starts listening; each accepted connection is handed to onConnection. Resolves once bound, with the address actually listening on. */ listen: ( address: string, onConnection: (connection: Readonly) => void, - ) => Promise<() => Promise>; + ) => Promise; } diff --git a/ts/packages/core/test/tcp-transport.test.ts b/ts/packages/core/test/tcp-transport.test.ts index 1ba2b61..d9cb29d 100644 --- a/ts/packages/core/test/tcp-transport.test.ts +++ b/ts/packages/core/test/tcp-transport.test.ts @@ -3,9 +3,6 @@ import { describe, expect, it } from "vitest"; import { createTcpTransport } from "../src/adapters/tcp-transport.js"; const LENGTH_PREFIX_BYTES = 4; -// A fixed address rather than port 0: the Transport port's listen takes the caller's address and never reports the bound one, so an OS-assigned port would be undiscoverable through the public surface this test deliberately exercises. -const TEST_LISTEN_ADDRESS = "127.0.0.1:44833"; -const TEST_LISTEN_PORT = Number(TEST_LISTEN_ADDRESS.split(":")[1]); /** Connects a raw socket (deliberately NOT the transport's own Connection -- the point is to write hostile bytes a well-behaved peer would never produce) and writes one length-prefixed body. */ async function writeRawBody(port: number, body: Uint8Array): Promise { @@ -31,36 +28,32 @@ describe("createTcpTransport", () => { const receivedError = new Promise((resolve) => { resolveError = resolve; }); - const stopListening = await transport.listen( - TEST_LISTEN_ADDRESS, - (connection) => { - void (async () => { - const frames: unknown[] = []; - for await (const frame of connection.receive()) { - frames.push(frame); - } - return frames; - })().then( - () => { - // A non-Error marker: the assertion below distinguishes a genuine rejection from clean completion - resolveError("COMPLETED"); - }, - (error: unknown) => { - resolveError(error); - }, - ); - }, - ); + // Port 0: the OS assigns a free port and the listener reports it back, so concurrent CI runs can never collide on a fixed one + const listener = await transport.listen("127.0.0.1:0", (connection) => { + void (async () => { + const frames: unknown[] = []; + for await (const frame of connection.receive()) { + frames.push(frame); + } + return frames; + })().then( + () => { + // A non-Error marker: the assertion below distinguishes a genuine rejection from clean completion + resolveError("COMPLETED"); + }, + (error: unknown) => { + resolveError(error); + }, + ); + }); // A lone top-level CBOR BREAK byte: decode() throws on it inside the socket data handler -- previously an uncaughtException that killed the process - await writeRawBody( - TEST_LISTEN_PORT, - Uint8Array.from(Buffer.from("ff", "hex")), - ); + const assignedPort = Number(listener.address.split(":")[1]); + await writeRawBody(assignedPort, Uint8Array.from(Buffer.from("ff", "hex"))); const error = await receivedError; expect(error).toBeInstanceOf(Error); - await stopListening(); + await listener.close(); }); });