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: 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. 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 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..bd8cbf5 --- /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, 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 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 + +```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..4ca0b0a --- /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 ts-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..18ee938 --- /dev/null +++ b/ts/packages/core/src/adapters/tcp-transport.ts @@ -0,0 +1,181 @@ +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, Listener, 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. 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: { + 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) { + 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); + + 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.resolve({ 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.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); + } + } + + socket.on("data", (chunk: Buffer) => { + buffer = Buffer.concat([buffer, chunk]); + tryDrain(); + }); + 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]() { + return { + async next(): Promise> { + const next = pending.shift(); + 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, reject) => { + waiters.push({ resolve, reject }); + }); + }, + }; + }, + }; +} + +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): Promise { + 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, () => { + 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/domain/handshake.ts b/ts/packages/core/src/domain/handshake.ts new file mode 100644 index 0000000..072e459 --- /dev/null +++ b/ts/packages/core/src/domain/handshake.ts @@ -0,0 +1,40 @@ +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; + +/** + * 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 and that are not retired -- 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) => + !RETIRED_DOMAINS.includes(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..d62e862 --- /dev/null +++ b/ts/packages/core/src/domain/tokens.ts @@ -0,0 +1,251 @@ +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, issuer: DeviceId) => 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; +} + +/** 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; + 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, + 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 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" }; + } + + 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" }; + } + 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"], claims.issuer)) { + return { ok: false, reason: "revoked" }; + } + + if (claims.parent !== undefined) { + 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 verifyTokenChain(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 (!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 }; +} + +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" }; + } + + 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" }; + } + 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/src/generated/protocol.ts b/ts/packages/core/src/generated/protocol.ts new file mode 100644 index 0000000..b79cdd1 --- /dev/null +++ b/ts/packages/core/src/generated/protocol.ts @@ -0,0 +1,316 @@ +// 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(), + "mailboxes": z.array(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 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)])); +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 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)), +})); +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 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; +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 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 RevocationClaims = 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; +export type CoordinatorFrame = 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..dcf9bac --- /dev/null +++ b/ts/packages/core/src/ports/transport.ts @@ -0,0 +1,27 @@ +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 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. Resolves once bound, with the address actually listening on. */ + listen: ( + address: string, + onConnection: (connection: Readonly) => void, + ) => 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..127b12a --- /dev/null +++ b/ts/packages/core/test/handshake.test.ts @@ -0,0 +1,64 @@ +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("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"]), + handshake(1, ["core/management"]), + ); + expect(result.ok).toBe(true); + }); +}); 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); + }); +}); 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..d9cb29d --- /dev/null +++ b/ts/packages/core/test/tcp-transport.test.ts @@ -0,0 +1,59 @@ +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; + +/** 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; + }); + // 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 + 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 listener.close(); + }); +}); diff --git a/ts/packages/core/test/tokens.test.ts b/ts/packages/core/test/tokens.test.ts new file mode 100644 index 0000000..1559872 --- /dev/null +++ b/ts/packages/core/test/tokens.test.ts @@ -0,0 +1,983 @@ +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, + verifyRevocationEntry, + 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, + 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. */ +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 }; +} + +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; + 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 token revoked by its own issuer's entry", async () => { + const tokenId = nextTokenId(); + const token = await signToken(issuer, { + tokenId, + bearer: bearerDeviceId, + scope: workScope, + expires: now + HOUR_MS, + }); + 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: 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(), + 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" }); + }); + + 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" }); + }); + + // -- 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 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" }); + + 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", + }); + }); + + 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, { + 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", () => { + 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); + }); + + 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", () => { + 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..72e964e --- /dev/null +++ b/ts/packages/core/turbo.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://turborepo.com/schema.json", + "extends": ["//"], + + "tasks": { + "_build": { + "inputs": ["src/**", "tsdown.config.ts", "tsconfig.json"], + "outputs": ["dist/**"] + }, + "_generate": { + "dependsOn": ["_build"], + // 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"] + }, + "_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..a07dab3 --- /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/8de0f0cf0e8eb14e347894a55bdb655d5f332bc5 + 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/8de0f0cf0e8eb14e347894a55bdb655d5f332bc5: + resolution: {tarball: https://codeload.github.com/ExaDev/cddl.js/tar.gz/8de0f0cf0e8eb14e347894a55bdb655d5f332bc5} + 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/8de0f0cf0e8eb14e347894a55bdb655d5f332bc5: + 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"] + } + } +}