diff --git a/.github/scripts/verify-release-boundary.mjs b/.github/scripts/verify-release-boundary.mjs index 76a862a..3913f47 100644 --- a/.github/scripts/verify-release-boundary.mjs +++ b/.github/scripts/verify-release-boundary.mjs @@ -16,7 +16,7 @@ const releasePolicy = await readFile( ); const workflowSha256 = createHash('sha256').update(workflow).digest('hex'); const expectedWorkflowSha256 = - '9177c8dbf0a75c85e79389f24390c0d83bdad8937a763ca1fa661ac3064f1315'; + '7c3e6bfe5e9697fc2122dcc29d39387f2d78a224178471ab530ec04dec8e48de'; const build = job('build-and-verify', 'publish'); const publish = job('publish', 'publish-release'); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b23f256..8228018 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,8 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: npm + - name: Materialize checksum-bound local peer artifacts + run: node scripts/materialize-peer-artifacts.mjs - run: npm ci - run: npm run check @@ -43,6 +45,8 @@ jobs: with: node-version: 24 cache: npm + - name: Materialize checksum-bound local peer artifacts + run: node scripts/materialize-peer-artifacts.mjs - run: npm ci - name: Verify LF policy and formatting shell: pwsh diff --git a/.github/workflows/node-sqlite-parity.yml b/.github/workflows/node-sqlite-parity.yml index 898ba32..421373f 100644 --- a/.github/workflows/node-sqlite-parity.yml +++ b/.github/workflows/node-sqlite-parity.yml @@ -17,18 +17,8 @@ jobs: with: node-version: 24 cache: npm + - name: Materialize checksum-bound local peer artifacts + run: node scripts/materialize-peer-artifacts.mjs - run: npm ci - - name: Pack the pinned Diamond source - env: - DIAMOND_COMMIT: 326da7e48d77cddc5a58e21cf65d872ecb9b3b74 - run: | - git clone --filter=blob:none https://github.com/gnolith/diamond.git "$RUNNER_TEMP/diamond" - git -C "$RUNNER_TEMP/diamond" checkout --detach "$DIAMOND_COMMIT" - test "$(git -C "$RUNNER_TEMP/diamond" rev-parse HEAD)" = "$DIAMOND_COMMIT" - npm ci --prefix "$RUNNER_TEMP/diamond" - npm pack "$RUNNER_TEMP/diamond" --pack-destination "$RUNNER_TEMP" - name: Verify packed-artifact node:sqlite parity - env: - DIAMOND_TARBALL: ${{ runner.temp }}/gnolith-diamond-0.4.0.tgz - DIAMOND_TARBALL_SHA256: ac8e34810a8504bd891b36a199973f2d65ef652555644813c7a36f1c7016c9d2 - run: node scripts/node-sqlite-parity.mjs + run: npm run parity:node-sqlite diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f696956..c2cca06 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,6 +80,8 @@ jobs: with: node-version: 24 cache: npm + - name: Materialize checksum-bound local peer artifacts + run: node scripts/materialize-peer-artifacts.mjs - run: npm ci - run: npm run check - run: npm run release:check -- "${{ github.ref_name }}" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..f8eaacb --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,58 @@ +name: Security + +on: + workflow_dispatch: + pull_request: + push: + branches: [main] + schedule: + - cron: '23 8 * * 1' + +permissions: + actions: read + contents: read + pull-requests: read + security-events: write + +jobs: + dependency-review: + if: github.event_name == 'pull_request' && github.event.repository.private == false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + cache: npm + - name: Materialize checksum-bound local peer artifacts + run: node scripts/materialize-peer-artifacts.mjs + - run: npm ci + - run: npm run build + - run: npm run security:dev-check + + secret-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - name: Install checksum-pinned GitLeaks CLI + env: + GITLEAKS_VERSION: 8.30.1 + GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb + run: | + archive="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + curl --fail --location --silent --show-error \ + --output "$RUNNER_TEMP/$archive" \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/$archive" + echo "$GITLEAKS_SHA256 $RUNNER_TEMP/$archive" | sha256sum --check - + tar -xzf "$RUNNER_TEMP/$archive" -C "$RUNNER_TEMP" gitleaks + - name: Scan complete Git history for secrets + run: | + "$RUNNER_TEMP/gitleaks" git --config .gitleaks.toml --redact --verbose --no-banner . diff --git a/.github/workflows/taproot-peer-conformance.yml b/.github/workflows/taproot-peer-conformance.yml index 0793f61..a161150 100644 --- a/.github/workflows/taproot-peer-conformance.yml +++ b/.github/workflows/taproot-peer-conformance.yml @@ -17,38 +17,8 @@ jobs: with: node-version: 24 cache: npm + - name: Materialize checksum-bound local peer artifacts + run: node scripts/materialize-peer-artifacts.mjs - run: npm ci - - name: Pack the authorization-foundation Taproot source - env: - TAPROOT_COMMIT: 819fe054ebb867e1ca92518bfd3b1aa6c5aa277d - run: | - git clone --filter=blob:none https://github.com/gnolith/taproot.git "$RUNNER_TEMP/taproot" - git -C "$RUNNER_TEMP/taproot" checkout --detach "$TAPROOT_COMMIT" - test "$(git -C "$RUNNER_TEMP/taproot" rev-parse HEAD)" = "$TAPROOT_COMMIT" - npm ci --prefix "$RUNNER_TEMP/taproot" - npm pack "$RUNNER_TEMP/taproot" --pack-destination "$RUNNER_TEMP" - - name: Install exact packed peers - run: | - workshop_version="$(node -p "require('./package.json').version")" - npm pack . --pack-destination "$RUNNER_TEMP" - mkdir "$RUNNER_TEMP/peer-consumer" - cd "$RUNNER_TEMP/peer-consumer" - npm init --yes - npm install --legacy-peer-deps --ignore-scripts --no-audit --no-fund \ - "$RUNNER_TEMP/gnolith-taproot-0.4.0.tgz" \ - "$RUNNER_TEMP/gnolith-workshop-${workshop_version}.tgz" \ - "@gnolith/diamond@0.4.0" \ - "miniflare@4.20260714.0" - cp "$GITHUB_WORKSPACE/.github/scripts/taproot-peer-conformance.ts" conformance.ts - cp "$GITHUB_WORKSPACE/.github/scripts/taproot-runtime-conformance.mjs" conformance.mjs - cp "$GITHUB_WORKSPACE/.github/scripts/taproot-d1-runtime-conformance.mjs" conformance-d1.mjs - - name: Verify exact typed boundary between packed peers - run: | - cd "$RUNNER_TEMP/peer-consumer" - node "$GITHUB_WORKSPACE/node_modules/typescript/bin/tsc" \ - --noEmit --strict --skipLibCheck --target ES2022 \ - --module NodeNext --moduleResolution NodeNext conformance.ts - - name: Verify packed runtime authorization guard - run: node "$RUNNER_TEMP/peer-consumer/conformance.mjs" - - name: Verify packed persisted D1 authorization guard - run: node "$RUNNER_TEMP/peer-consumer/conformance-d1.mjs" + - name: Verify the exact assembled packed process + run: npm run packed:process-check diff --git a/.gitignore b/.gitignore index db2990a..82f84e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules/ dist/ coverage/ +.artifacts/ .wrangler/ .release/ *.tgz diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..57e9d79 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,11 @@ +title = "Workshop secret scanning" + +[extend] +useDefault = true + +[[allowlists]] +description = "Known non-secret item idempotency fixture" +condition = "AND" +regexTarget = "secret" +regexes = ['''^item-subject-v1$'''] +paths = ['''(^|/)test/search\.integration\.test\.ts$'''] diff --git a/.prettierignore b/.prettierignore index 7706f1f..3c689e5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,2 @@ -node_modules/ -dist/ -coverage/ -.wrangler/ -*.tgz +src/generated/** +schemas/*.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a3f8d50..9fb5d8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # Changelog +## [0.5.0] + +- Replaced the UI/Site/Worker package with the `gnolith-workshop` self-hosted + executable, one listener, authenticated MCP and BFF, and explicit offline + bootstrap. +- Generated one 52-operation `gnolith` catalog shared by schemas, MCP, BFF, + validation, and the generic client. +- Added separate local-bearer and remote-OAuth profiles, fail-closed readiness, + verified immutable Waystone assets, bounded lifecycle controls, task leases, + and owner-scoped backup APIs. +- Added a read-only, bounded exact-0.4.2 legacy-owner decoder and owner-bound + adoption adapter with opaque fragments, migration-ledger evidence, digests, + and Task/Memory/Prompt/revision contribution counts. +- Coupled task lease claim, release, and completion atomically to canonical + source/history updates, durable idempotency receipts, and lease audit events. +- Added schema 8 durable credential/bootstrap receipts and an exact Taproot + authorization-bootstrap owner adapter. +- Added schema 11 sealed authorization administration, durable principal, + membership, credential/OAuth, audit, and semantic configuration state. +- Added offline semantic configuration with exact revision/fingerprint + receipts, fail-closed endpoint policy, and truthful degraded/ready status. +- Added public bounded local-filesystem ResourceBlob backup/restore ports with + private content-addressed paths, verified staging, collision checks, rebinds, + rollback, and symlink/traversal defenses. +- Made owner restore collision-safe and repeatable on a fresh current target, + preserving one canonical copy of task idempotency receipts, revisions, and + outcomes while rejecting inconsistent duplicates. +- Distinguished schema-declared authorization administration targets from the + transport-authenticated caller, serialized manual and automatic search + materialization, fenced simultaneous task claims, and rejected encoded + asset traversal before Node URL normalization. +- Defined the exact domain-separated local-bearer credential digest in the + public operation schema, exposed its safe digest helper, hardened + recursively encoded dot-segment rejection, and returned bounded manual + materialization job/result/health evidence. +- Removed legacy operation aliases, UI and CSS exports, Site route factories, + stdio transport, and the parallel hand-written MCP catalog. + All notable changes follow Keep a Changelog and Semantic Versioning. ## [0.4.2] - 2026-07-22 diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 281a201..ba5ba89 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -1,48 +1,20 @@ # Compatibility -Workshop 0.4.x targets: +Workshop 0.5 is a breaking reset and requires Diamond and Taproot 0.5 candidate +APIs. It supports Node.js 22 or newer with native SQLite or an injected D1-like +persistence adapter. -- Diamond `>=0.4.0 <0.5.0` through injected SQLite/query services -- Taproot `>=0.4.0 <0.5.0`; this breaking line includes canonical external - Task, Memory, and Prompt producer registration, sealed atomic mutations, - bounded legacy adoption, authorized hydration, durable materialization, and - semantic-search administration. -- Waystone plugin contract `>=0.1.0 <0.3.0`; released Waystone 0.2.x accepts - Workshop's structural plugin shape through its public - `WorkshopCompatibleWaystonePlugin` and `createWaystoneRegistry` boundary. -- React `>=19 <20` -- TypeScript `>=5.9 <6` -- MCP protocol `2025-11-25`, with `2025-06-18` and `2025-03-26` negotiation -- MCP TypeScript SDK `1.29.x` for interoperability verification -- vinext `1.0.0-beta.2` (isolated package-consumer target) -- Cloudflare Workers compatibility date `2026-07-20` or later -- Node.js 22 or 24 for development and release tooling +Removed surfaces: `@gnolith/workshop/ui`, `@gnolith/workshop/site`, CSS exports, +Worker-specific handlers, stdio MCP, and the legacy tool dispatcher. Removed +operation aliases are documented only in +`schemas/legacy-operation-names-v1.json` and are never accepted at runtime. -The exact Workshop tarball compiles in isolated vinext/Vite and Worker -consumers. A separate disposable consumer installs the exact tarball with -public Waystone 0.2.0 and React 19 through normal npm peer resolution, compiles -the public declarations, and registers the Workshop plugin through Waystone's -public compatibility adapter. These fixtures inject or stub Diamond, Taproot, -identity, and host bindings; they verify Workshop package compatibility only -and perform no remote deployment. They do not assemble or qualify a complete -Gnolith Site. +Waystone is optional static content. Workshop serves only a verified immutable +`waystone-assets-v1` manifest and does not import or initialize a Waystone +runtime. The canonical safe mount is `/app`; service and protocol routes remain +reserved. -The generic exact-package declaration consumer also compiles the required -Task/Memory history methods on `WorkshopClient` and their process-core service -signatures, including the shared bounded history options. Its public-version -mode repeats normal npm resolution after publication. - -CI also packs released Taproot 0.4.0 source commit -`819fe054ebb867e1ca92518bfd3b1aa6c5aa277d` and checks bidirectional structural -compatibility for authorization contexts and visibility scopes, the real -authorized reader, host-capability-bound guards, and all three Workshop search -producer domains. The packed runtime lane exercises native SQLite and persisted -Miniflare D1, including D1 disposal/recreation and cursor continuation. It also -asserts that raw repository/deep-result surfaces are absent. This -package-to-package lane detects contract drift without provisioning or accepting -a complete Site. - -The Codex agent creating a Site owns the compatibility of the complete host -bundle, including its chosen peer entry points, runtime flags, infrastructure, -and deployment configuration. Compatibility range changes still require a -documented Workshop release. +Semantic configuration accepts OpenAI-compatible and Ollama-compatible +providers with SQLite or Qdrant vectors. Ollama remains degraded/unavailable +until a separately supplied immutable model is actually ready; Workshop never +pulls a floating model or invents a model digest. diff --git a/README.md b/README.md index 365b9a2..51723ef 100644 --- a/README.md +++ b/README.md @@ -1,209 +1,121 @@ -# Workshop +# @gnolith/workshop -`@gnolith/workshop` is Gnolith's agent-facing operating layer: durable research -tasks, ephemeral task packets, reusable memories, canonical prompts, -authorization-aware unified search and graph access, Taproot-backed knowledge -tools, Streamable HTTP MCP, Worker route factories, and the Workshop Waystone UI. +Workshop 0.5 is the self-hosted Gnolith executable. It owns one HTTP listener, +the authenticated `/mcp` endpoint, authenticated `/health/ready`, live +`/health/live`, and the `/api/v1/operations/*` BFF. -One Codex Site is one research project. Workshop operates inside that Site-wide -graph and task system; it does not add projects, memberships, agent sessions, -packet snapshots, or a background coordinator. +The release is intentionally headless. The former React UI, Site route +factories, Worker runtime, stdio transport, and hand-written parallel tool +catalog are not part of 0.5. -## Runtime surfaces +## Executable -| Import | Runtime | Purpose | -| ------------------------------ | -------------- | ----------------------------------------------------- | -| `@gnolith/workshop` | any | Package identity and safe shared types | -| `@gnolith/workshop/core` | any | Process-local services and authorized tool dispatch | -| `@gnolith/workshop/protocol` | browser/Worker | Models, errors, and injectable HTTP client | -| `@gnolith/workshop/server` | Worker/process | Persistence services, health, and HTTP adapters | -| `@gnolith/workshop/mcp` | any/Worker | Neutral tool dispatcher and stateless HTTP MCP server | -| `@gnolith/workshop/site` | Worker | Thin App Router-compatible route factories | -| `@gnolith/workshop/ui` | browser | React 19 components and `workshopPlugin` | -| `@gnolith/workshop/migrations` | installer | Embedded canonical migration manifest | -| `@gnolith/workshop/styles.css` | browser | Workshop-specific styles | - -The root export intentionally does not import all runtimes. +```sh +gnolith-workshop serve --config /run/gnolith/workshop.json +``` -## Process-local integration +The JSON config uses format `gnolith-workshop-config-v1`, selects exactly one +authentication profile (`local-bearer-v1` or `remote-oauth-v1`), declares an +explicit listener profile, and points to an adapter module that supplies the +Diamond and Taproot public ports. Startup is fail-closed: it does not apply +migrations or bootstrap authorization. -Core services run directly in a process without HTTP, a UI, or a listening -server. The host injects one structural SQLite persistence capability, the same -live atomic authorization authority used by Taproot, an opaque cursor codec, -and read-authorized knowledge/health adapters: +Offline first-owner bootstrap never opens a socket: -```ts -import { createWorkshopCore } from '@gnolith/workshop/core'; -import { createWorkshopSearchIntegrationV1 } from '@gnolith/workshop/server'; - -const search = await createWorkshopSearchIntegrationV1({ - db: persistence, - taproot, - hostCapability, - installationId, - registrationContext: principal, -}); - -const workshop = createWorkshopCore({ - persistence, - authorization, - cursorCodec, - knowledge: { authorizedReader, health: taprootHealth }, - diamondHealth, - search, -}); +```sh +gnolith-workshop bootstrap-v1 \ + --config /run/gnolith/workshop.json \ + --installation-id installation:example \ + --base-iri https://example.invalid/ \ + --principal principal:owner \ + --workspace workspace:default \ + --credential-file /run/secrets/workshop-token \ + --idempotency-key initial-owner-v1 ``` -Workshop owns its exact migrations and service behavior. Seedbed owns database -paths, adapter lifecycle, stdio/CLI wiring, Docker, and package assembly. +The local bearer credential file must contain exactly 32 random bytes encoded +as unpadded base64url, optionally followed by one LF. The token is never a +command argument and only its domain-separated digest is persisted. +Bootstrap requires Workshop schema version 11 and commits that credential digest +plus an owner receipt through Taproot's sealed owner-adapter API. +For `authorization_admin` `credential-register`, callers supply only the +lowercase hexadecimal digest defined in the operation schema: +`SHA-256(UTF-8("gnolith-local-bearer-v1\0") || base64urlDecode(token))`. +`digestLocalBearerCredentialText` from `@gnolith/workshop/server` computes that +value. The operation returns only credential ID metadata; plaintext bearer +material is never returned, persisted, or written to administration audit data. -## Install +Offline semantic configuration also runs before the listener: ```sh -npm install @gnolith/workshop @gnolith/diamond @gnolith/taproot react +gnolith-workshop configure-semantics-v1 \ + --config /run/gnolith/workshop.json \ + --input /run/gnolith/semantic-configuration.json ``` -Workshop targets Node.js 22+ for development and Web API runtimes. Its published -package is checked in isolated Worker and vinext consumers without the Workers -Node compatibility flag. Those consumers inject or stub peer services and do -not represent a complete Gnolith Site. - -## Site integration - -Create the runtime once from host-provided D1, identity, Diamond, and Taproot -services. Authentication belongs to the Site; role labels on tasks never grant -permissions. +The input contains only declarative provider/vector identities and protected +credential selector IDs. It never contains credential bytes. Configuration can +be persisted while unavailable; `gnolith_status.semanticState.ready` becomes +true only after the selected generation is actually ready, while lexical search +remains available. `provider.endpoint` is the exact POST URL: an +OpenAI-compatible endpoint ends in `/embeddings` (for example +`https://provider.example/v1/embeddings`) and an Ollama-compatible endpoint +ends in `/api/embed`. A configured but degraded semantic provider makes +`/health/ready` return 503 without preventing the process from starting or +serving lexical search. + +`gnolith_status.semanticState.diagnostic` is either `null` or the exact +secret-safe `gnolith-workshop-semantic-diagnostic-v1` object. The versioned +object reports only a bounded code, retryability, `sqlite|qdrant` backend, and +the bounded `retry` repair class. It never includes URLs, credential selectors, +paths, provider messages, or recursive causes. Exact downstream fixtures are +published at `schemas/fixtures/workshop-status-evidence-v1.json`. + +Authorization visibility uses canonical CNF. The `clauses` array is ANDed, +while atoms inside one clause are ORed. Owner-or-reader is one clause containing +both principal atoms. Two singleton clauses mean owner-and-reader and are +rejected when the administering principal cannot satisfy the resulting scope. + +Verified Waystone assets should use the exported `/app` mount recommendation. +The root, `/mcp`, `/health`, `/api`, and `/.well-known` namespaces cannot be +used or shadowed by an asset mount. The Node listener rejects encoded dot +segments and encoded path separators before URL normalization. + +Local resource payload backup and restore use the public server-only factory: ```ts -import { createWorkshopRuntime } from '@gnolith/workshop/server'; - -export const workshop = createWorkshopRuntime({ - db: env.DB, - authorization: siteAuthorization, - cursorCodec: siteCursorCodec, - knowledge: { - authorizedReader: createAuthorizedReader, - health: taprootHealth, - }, - diamondHealth: siteDiamond.health, - resolvePrincipal: authenticateWorkshopRequest, - search: workshopSearch, +import { createLocalFilesystemResourceBlobPortsV1 } from '@gnolith/workshop/backup'; + +const blobs = await createLocalFilesystemResourceBlobPortsV1({ + root: '/var/lib/gnolith/blobs', }); ``` -`workshopSearch` is created once with `createWorkshopSearchIntegrationV1` after -Taproot and Workshop migrations. The factory initializes Taproot's durable -materialization state before registering the Task, Memory, and Prompt producers. -All three domains commit the Workshop mutation, immutable revision snapshot, -and canonical source event through Taproot's sealed atomic boundary. Legacy -adoption and rebuilds are explicit bounded `search:admin` operations. +The returned `backup` and `restore` ports accept only Taproot descriptors, +streams, stage IDs, and opaque reference IDs. Filesystem layout remains private +to Workshop. -`siteAuthorization` remains the single shared live source for authorized reads, -backfill maintenance, and cursor snapshots. Host-issued guards must preserve -exact capabilities and never add or translate them. -`createAuthorizedReader` must return the public `AuthorizedTaprootReader` bound -to that same source. Knowledge mutations are unavailable until that shared -foundation is complete. +Whole-owner restore uses `createWorkshopBackupApi` from the same export. +Orchestrators pass the opaque document back to `importBackup`; Workshop +canonicalizes logical records, atomically replaces only the bound +installation's owner state, and rejects inconsistent duplicates. -Generated App Router files stay thin: +## Protocol -```ts -import { createWorkshopMcpHandler } from '@gnolith/workshop/site'; -import { workshop } from '@/lib/gnolith/server'; - -const handler = createWorkshopMcpHandler(workshop); -export const GET = handler; -export const POST = handler; -``` +The sole MCP identity is `gnolith`. The generated operation contract contains +exactly 52 versioned operations and is checked into +`schemas/gnolith-operations-v1.json`. The same generated registry drives MCP, +the BFF, runtime validation, and the generic browser-safe client: -Consumers apply migrations explicitly before constructing the runtime. Runtime -construction never creates tables. `workshopMigrations` provides canonical SQL -and checksums; `applyWorkshopMigrations(persistence)` applies only -Workshop-owned schema through Diamond's shared namespaced checksum ledger when -the host chooses to initialize or migrate. - -## Core behavior - -- Task creation validates every field, statically rejects SPARQL writes and - unsafe query forms, and verifies all memory references before inserting. -- `claim_task` is one conditional SQLite update. Concurrent callers cannot both win. -- Completion is one conditional update and accepts negative or inconclusive - nonempty results. -- Task packets resolve authorized current memories on every read. Unscoped - context-query execution is disabled until the host supplies a scoped graph - boundary. Packet reads do not claim or mutate tasks. -- Task, Memory, and Prompt histories are bounded newest-first canonical - snapshots. Each snapshot is authorized independently, and the exact current - record plus live installation authorization are rechecked immediately before - content returns. -- Knowledge reads use Taproot's authorized-reader boundary. Mutations are - fail-closed and are not advertised by Workshop's MCP surface. -- MCP authenticates every request and authorizes every tool server-side. -- Detailed diagnostics require exact `admin`; abandoned-claim reset requires - exact `admin` plus `task-write`. Capabilities never imply one another. These - operations are not normal MCP tools. - -## Development - -```sh -npm ci -npm run check -``` +```ts +import { createWorkshopClient } from '@gnolith/workshop/client'; -The gate covers formatting, lint, strict types, coverage, local D1 integration -and concurrency, MCP, routes, interactive UI, build, performance, generic -exact-tarball validation, isolated Worker and vinext package consumers, audit, -readiness, and release-artifact invariants. One generated tarball and its -machine-verifiable provenance are reused by every package consumer and release -check. Peer services in the runtime consumers are injected or stubbed. See -[`docs/release-provenance.md`](docs/release-provenance.md). - -Configure the Waystone contribution with a browser-only client. Construction is -lazy, so importing the UI never initializes a server runtime or captures host -bindings: - -```tsx -import { createWorkshopClient } from '@gnolith/workshop/protocol'; -import { createWorkshopPlugin } from '@gnolith/workshop/ui'; - -export const workshopUi = createWorkshopPlugin({ - client: () => - createWorkshopClient({ - baseUrl: '', - token: () => sessionToken(), - }), - capabilities: currentCapabilities, - loadMcpStatus: loadWorkshopMcpStatus, +const client = createWorkshopClient({ + baseOrigin: 'https://gnolith.example', + accessToken: async () => obtainAccessToken(), }); +const status = await client.call('gnolith_status', {}); ``` -Legacy onboarding journals remain quarantined and are not exposed by the -public core, protocol, MCP, HTTP, or UI surfaces. A separately scoped reusable -exploration skill will own future onboarding behavior. - -`Workshop package handoff ready` means these package-owned gates pass against -the exact tarball. It does not qualify a complete Gnolith Site. The Codex agent -creating a Site owns four-package assembly, infrastructure and migrations, -identity and secrets, deployment configuration, live browser/MCP/Codex probes, -and final acceptance. - -## Documentation - -- [Architecture and domain semantics](docs/architecture.md) -- [HTTP and client API](docs/http-api.md) -- [MCP tools](docs/mcp.md) -- [Authentication and authorization](docs/authentication.md) -- [Migrations and upgrades](docs/migrations.md) -- [Authorization foundation ledger](docs/authorization-foundation.md) -- [Waystone UI](docs/ui-and-onboarding.md) -- [Health and semantic verification](docs/verification.md) -- [Operational limits](docs/limits.md) -- [Security and threat assumptions](docs/security.md) -- [Codex Sites compatibility](docs/codex-sites.md) -- [Troubleshooting](docs/troubleshooting.md) -- [Release checklist and current evidence](docs/release-checklist.md) -- [Package handoff contract](docs/package-handoff.md) - -## License - -MIT +See [the architecture-reset guide](docs/architecture-reset.md) for deployment, +authentication, migration, lease, backup, and compatibility details. diff --git a/docs/architecture-reset.md b/docs/architecture-reset.md new file mode 100644 index 0000000..3a94ffc --- /dev/null +++ b/docs/architecture-reset.md @@ -0,0 +1,64 @@ +# Workshop 0.5 architecture reset + +Workshop is one process with one HTTP listener. `/health/live` is a +dependency-free process probe. `/health/ready`, `/mcp`, and the BFF authenticate +before reading or disclosing protected state. Readiness reports migration, +authorization, canonical data, lexical index, producer, blob, dependency +compatibility, version, and operation-catalog digest state. + +Container binds must be explicit, non-loopback, and non-ephemeral. Remote binds +must use an explicit port. Only the test profile permits port zero. Shutdown +stops acceptance, drains requests, aborts and checkpoints bounded background +work, and closes injected dependencies. + +`local-bearer-v1` and `remote-oauth-v1` are separate profiles with no fallback. +Local authentication accepts only the Authorization bearer header, compares a +domain-separated digest in constant time, and rechecks the live authorization +fence. OAuth validates signature through an injected verifier and then enforces +issuer, audience, allowed algorithm, time bounds, required scopes, server-owned +scope mappings, and live principal resolution. + +Migrations are explicit operator actions. Schema version 7 adds opaque, +expiring task leases, owner and revision fencing, idempotent release receipts, +and lease audit events. Schema version 8 adds durable local credential records +and owner-bound bootstrap receipts. Schema version 9 adds durable idempotency +receipts for prompt mutations and memory deletion. Schema version 10 adds +durable Task and Memory mutation receipts. Schema version 11 adds the +authorization-administration and semantic-configuration control planes. Raw +bearer and lease tokens are never stored. +Backup APIs operate through Workshop owner and migration-ledger handles, report +legacy counts during inspection, and expire imported task leases. + +Local resource payloads use +`createLocalFilesystemResourceBlobPortsV1` from +`@gnolith/workshop/backup`. The factory receives one absolute, operator-owned +root and returns Taproot's `ResourceBlobBackupPort` and +`ResourceBlobRestorePort`. Reference IDs stay opaque: neither Seedbed nor an +operator runner derives a path from them. Workshop privately manages the +content-addressed objects, manifest, staging, verification, collision checks, +canonical rebinds, and rollback beneath that root. + +The operation registry is generated from one source and validated with Ajv at +the central operation layer. MCP and BFF adapters cannot add operations or +aliases. Search results use exactly seven public kinds: `statement`, `item`, +`task`, `memory`, `prompt`, `resource`, and `annotation`. Property results use +kind `item` with `entityType: "property"`. + +Task, Memory, and Prompt mutations cross Taproot's external-producer owner +boundary with one-shot Workshop statement tokens. On update, Workshop reads the +sealed predecessor from the exact registered producer and passes the returned +frozen event identity and sequence unchanged to the next issuer seal. Workshop +does not inspect Taproot search tables or reconstruct predecessor state. + +Exact Workshop 0.4.2 owner state is transferred through the public +`@gnolith/workshop/backup` library. `decodeWorkshopLegacyOwnerV042` accepts a +read-only portable database capability plus exact package/schema and +Workshop-namespace ledger attestation. It reads only a fixed Workshop-owned +table allowlist and returns a bounded opaque fragment, SHA-256 evidence, table +counts, and explicit Task/Memory/Prompt/revision contribution counts. +`adoptWorkshopLegacyOwnerV042` passes only a verified fragment to an +owner-bound destination handle; it never mutates the source or reads Seedbed, +Diamond, or Taproot tables. + +Optional static content is served only after canonical manifest and per-file +SHA-256 verification. Reserved service prefixes cannot be shadowed. diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index d6f524b..0000000 --- a/docs/architecture.md +++ /dev/null @@ -1,65 +0,0 @@ -# Architecture and domain semantics - -Workshop sits above Diamond and Taproot. Diamond owns quads and query internals; -Taproot owns canonical entities, revisions, RDF projection, and unified-search -materialization; Workshop owns agent-facing tasks, memories, prompts, their -canonical producer adapters, HTTP/MCP, and UI. Workshop routes -knowledge reads only through Taproot's authorized reader. Knowledge mutations -are currently unavailable, and no Workshop path writes RDF directly. - -Reads use `AuthorizedTaprootReader` bound to the same live authorization source -that guards Task, Memory, and Prompt operations; raw repositories are not adapter -surfaces. A CI lane packs the exact released Taproot 0.4 producer contract -and compiles the public boundary. This is package-runtime compatibility, not -complete-Site assembly or acceptance. - -## Tasks - -Tasks persist title/objective, description, constraints, acceptance criteria, -relationships, assignment, language/attribution, advisory role, prompt, context -queries, memory slugs, claim fields, completion/archive timestamps, typed -outcome, and timestamps. State is derived in this order: archived, completed, -claimed, unclaimed. There is no status column, claim token, or ordinary unclaim. - -Create and update share a static context-query validator. It rejects update -forms and unsafe constructs without executing a graph query. Updates and -archives prefer `expectedRevision` and retain `expectedUpdatedAt` as a -compatibility token. When both are supplied, revision is authoritative. Claims -and completion use conditional `UPDATE ... RETURNING` statements through the -Taproot's sealed canonical mutation coordinator, so the Workshop row, revision -snapshot, and source event commit atomically. - -## Memories and Prompts - -Memories include title, applicability, provenance, language, attribution, -policy revision, soft deletion, and immutable history. Prompts are a first-class -canonical domain with ordered/priority metadata, variables, activation state, -visibility policy, soft deletion, and immutable history. Every write requires -the registered search integration and fails closed without it. - -## Unified search - -`createWorkshopSearchIntegrationV1` initializes Taproot materialization before -registering the Task, Memory, and Prompt producer descriptors. Producers use -bounded enumeration (at most 100), exact revision loads, deterministic -projections, policy-authority authorization, and current-reference hydration. -Legacy adoption and materialization/rebuild/semantic operations are explicit -`search:admin` actions; normal search never performs migration or repair. - -## Packets - -A packet is a response, never a row. Every read loads the current task, returns -a bounded fail-closed result for each stored context query, resolves current -authorized memories, rehydrates the owning Task, and records resolution time. -It never claims, executes graph queries, or refreshes in the background. - -## Memories - -Memories hold reusable methods, procedures, conventions, and checklists. Graph -facts belong in Taproot. Memories are slug-addressed, and every write advances -a monotonic revision. Durable idempotency keys are installation-scoped and -conflict if replayed with different content or visibility. - -Historical onboarding schema remains only so existing databases can be opened -and migrated. Onboarding is not a public Workshop service or UI surface in this -release. diff --git a/docs/authentication.md b/docs/authentication.md deleted file mode 100644 index 1de1b4e..0000000 --- a/docs/authentication.md +++ /dev/null @@ -1,37 +0,0 @@ -# Authentication and authorization - -The host supplies `ResolveWorkshopPrincipal(Request)` and one shared live -`WorkshopAuthorizationSource`. The resolved context is the exact Taproot -authorization shape: installation, principal, active workspace, workspace -memberships, capabilities, and authorization revision. Live installation state -also includes Taproot's search generation. The source must be the same authority -used by Taproot; Workshop does not own a second revision or generation clock. - -Every Task and Memory service operation requires the full context. Reads apply -installation and visibility predicates in SQL before content hydration, then -recheck both policy and live authorization state. A revision or membership -change therefore revokes access immediately, including empty-list and cursor -paths. Cursors are host-keyed, domain-separated, and bound to the complete -normalized grant context, query, filters, authorization revision, and search -generation. Every page hydrates and reauthorizes all `limit + 1` rows, including -the metadata-only sentinel, before returning content. - -Every Workshop capability is exact. `admin` grants only the administrative -operations that explicitly require `admin`; it does not imply `read`, -`task-write`, `memory-write`, `knowledge-write`, or `search:admin`. Principals -that need more than one operation must carry every corresponding capability. -Exact `search:admin` is required for the host-only legacy authorization -backfill. Legacy rows without complete metadata are quarantined and are not -visible through normal services. - -The host provides separate exact Taproot domain guards for Task writes, Memory -writes, Task authorization backfill, Memory authorization backfill, and cursor -snapshot creation. Maintenance and snapshot guards use the shared Taproot fence -without advancing authorization revision, search generation, or an advance ID. -Taproot authorization rejection maps to Workshop's generic `forbidden`; other -database and infrastructure failures remain distinct. - -Consumer browser authentication and machine/MCP bearer authentication may use -different host mechanisms but must resolve to the same transport-neutral -authorization shape. UI capability hints are presentation only. Never put access -tokens in tasks or memories. diff --git a/docs/authorization-foundation.md b/docs/authorization-foundation.md deleted file mode 100644 index 226b30f..0000000 --- a/docs/authorization-foundation.md +++ /dev/null @@ -1,51 +0,0 @@ -# Authorization foundation ledger - -Workshop 0.3.0 establishes the package-local authorization prerequisites for a -future unified search/indexing implementation. It does not claim that unified -search, indexing, or complete-Site acceptance is finished. - -## Implemented here - -- Exact Taproot-shaped authorization contexts and canonical CNF visibility. -- One host-injected live authorization source plus separate atomic Task and - Memory mutation methods backed by Taproot guards bound to exact `task-write` - and `memory-write`. Separate exact `search:admin` maintenance guards fence - Task and Memory authorization backfill, and an exact `read` guard fences - cursor snapshot creation. Workshop has no independent authorization table or - revision authority, and never elevates either domain to `knowledge:write`. - The host adapter maps Taproot authorization rejections to Workshop's generic - `forbidden` response; it must not disguise unrelated database failures as - authorization denials. -- Durable Task and Memory installation, owner, workspace, visibility, and - authorization-revision metadata. -- Installation/visibility SQL candidate filtering before hydration, followed by - policy and live-state rechecks. -- Durable bounded pagination snapshots containing only IDs, revisions, and - ordering metadata; host-keyed domain-separated binding digests; host-sealed - generation/query/domain/grant-bound cursor tokens; cross-installation - isolation; and immediate authorization/search-generation revocation. -- Fail-closed quarantine for legacy rows plus an explicit bounded, resumable, - host-only backfill requiring exact `search:admin`. -- Authorized Taproot entity reads and candidate search through the merged - `AuthorizedTaprootReader` contract. Knowledge writes and unscoped SPARQL - execution are unavailable through public Workshop operations. - -These changes advance the authorization portions of the system foundations -tracked as B03, D03, H01, and H07. They do not complete those broad foundations. - -## Deliberately not implemented - -- A unified Task/Memory/entity index or ranking algorithm. -- A scoped Diamond graph-query interface suitable for public Workshop SPARQL or - packet context queries. -- The host cursor-key lifecycle, rotation, and durable generation store. -- Public onboarding. Legacy journals are retained only for migration history - and are not exported or discoverable through core, HTTP, MCP, or UI surfaces. -- Site assembly, provisioning, deployment, or complete-system acceptance. - -Seedbed (or another host assembler) must inject the shared authorization source, -implement the five exact atomic Workshop guard ports against the same persisted -policy, provide the cursor codec and durable generation state, and coordinate -migrations. Workshop's narrow port is a package contract, not complete-Site -assembly or acceptance. Diamond must expose a scoped graph boundary before -Workshop can safely enable context-query execution. diff --git a/docs/breaking-0.5.md b/docs/breaking-0.5.md new file mode 100644 index 0000000..b87a516 --- /dev/null +++ b/docs/breaking-0.5.md @@ -0,0 +1,79 @@ +# Migrating to Workshop 0.5 + +Workshop 0.5 is an architecture reset. Replace Site/Worker route factories and +the React UI with the `gnolith-workshop` executable. Remove imports of +`@gnolith/workshop/site`, `@gnolith/workshop/ui`, and the CSS export. Waystone +may instead provide a verified immutable asset manifest mounted by the same +Workshop listener at the canonical `/app` prefix. + +Replace domain-specific browser-client calls with the generated generic client: + +```ts +await client.call('get_task', { taskId }); +``` + +Use `/api/v1/operations/` for BFF calls and `/mcp` for Streamable +HTTP MCP. There is no stdio transport, GET SSE listener, alternate MCP path, or +`workshop` server identity. + +The complete machine-readable old-name mapping is +`schemas/legacy-operation-names-v1.json`. Granular knowledge mutations map to +`mutate_entity`; old search maintenance names map to the discriminated +`search_admin`; SPARQL dry run maps to `query_sparql` with `dryRun: true`. +SPARQL Update and abandoned-claim reset have no replacement. + +Task claim handling now uses opaque expiring leases. Persist the returned lease +token only in protected worker state and supply it with the exact task revision +to update, complete, or idempotently release the lease. Never retry a release +with a different payload under the same idempotency key. + +Migrations are explicit. An operator runner calls the public Workshop migration +library after Diamond and Taproot migrations; `serve` will fail closed rather +than apply pending migrations. Run offline `bootstrap-v1` only after all owner +migrations succeed. + +Schema 11 is required for bootstrap because the Workshop owner adapter commits +the local credential digest and immutable bootstrap receipt in Taproot's sealed +authorization-bootstrap transaction. + +Run `configure-semantics-v1 --config --input ` after +bootstrap and before `serve`. The configuration file contains selector IDs, not +credential bytes. Readiness exposes the exact semantic revision, fingerprint, +and ready state without weakening lexical search. Provider endpoints are exact +POST URLs (`.../embeddings` for OpenAI-compatible providers and `.../api/embed` +for Ollama-compatible providers), not API base URLs. A persisted failed +configuration degrades readiness but does not stop or restart-loop the +Workshop service. + +Semantic readiness diagnostics use the exact versioned +`gnolith-workshop-semantic-diagnostic-v1` shape with bounded `code`, +`retryable`, `backend`, and `repair` fields. Ready and unconfigured states use +`diagnostic: null`. Consumers branch on `format` and `version`; generated +fixtures in `schemas/fixtures/workshop-status-evidence-v1.json` cover ready, +pending SQLite, unavailable Qdrant, and unavailable-after-restart states +without exposing endpoints or credentials. + +Visibility scopes are canonical CNF: clauses are ANDed and atoms within a +clause are ORed. An owner-or-reader policy is one clause with two principal +atoms. Two singleton clauses require both principals and are rejected if the +administering principal would lose visibility. + +For filesystem resource backup and restore, instantiate +`createLocalFilesystemResourceBlobPortsV1({ root })` from +`@gnolith/workshop/backup` and pass its `backup` and `restore` ports to the +Taproot/Seedbed orchestration. Do not construct blob paths from reference IDs. + +Installation restore remains owner-opaque. Call +`createWorkshopBackupApi(...).importBackup(document)` or the bound owner +handle's `importOwnedState(...)`; never parse Workshop tables in the +orchestrator. Import atomically replaces the bound installation's Workshop +state, restores each logical idempotency record once, preserves receipts, +revisions, and outcomes, expires live leases, and rejects inconsistent +duplicates. + +To transfer an existing exact 0.4.2 database, use +`decodeWorkshopLegacyOwnerV042` and `adoptWorkshopLegacyOwnerV042` from +`@gnolith/workshop/backup`. The decoder requires exact 0.4.2/schema-6 and +Workshop migration-ledger evidence, is read-only, and returns a bounded opaque +Workshop owner fragment with Task, Memory, Prompt, and owned-revision counts. +There is no legacy-adoption CLI and the adapter has no Seedbed table knowledge. diff --git a/docs/codex-sites.md b/docs/codex-sites.md deleted file mode 100644 index ec47170..0000000 --- a/docs/codex-sites.md +++ /dev/null @@ -1,17 +0,0 @@ -# Codex Site consumption boundary - -Workshop is ESM for vinext/Vite, React 19, Next.js App Router conventions, and -Cloudflare Workers. It uses Request, Response, streams, AbortSignal, Web Crypto, -TextEncoder, and performance APIs. Server state is D1-authoritative; runtime -migration discovery, filesystem scanning, lifecycle mutation, and Node built-ins -are absent from package runtime exports. - -The fixture under `examples/package-runtime-canary` bundles the exact Workshop -package with Wrangler without a Node compatibility flag. Its Diamond, Taproot, -identity, and host services are injected or stubbed. It is a non-deployable, -isolated package-runtime check, not a four-package Site canary. - -The Codex agent creating a Site owns assembly of Diamond, Taproot, Waystone, and -Workshop; infrastructure and migration application; host identity, secrets, -and configuration; deployment; live semantic/MCP/browser probes; and final Site -acceptance. Workshop release evidence does not cover those responsibilities. diff --git a/docs/http-api.md b/docs/http-api.md deleted file mode 100644 index 14d2bba..0000000 --- a/docs/http-api.md +++ /dev/null @@ -1,37 +0,0 @@ -# HTTP route factories and browser client - -`@gnolith/workshop/site` exports: - -- `createTasksHandler`, `createTaskHandler`, `createTaskPacketHandler` -- `createTaskClaimHandler`, `createTaskCompleteHandler`, `createTaskHistoryHandler` -- `createMemoriesHandler`, `createMemoryHandler`, `createMemoryHistoryHandler` -- `createPromptsHandler`, `createPromptHandler`, `createPromptHistoryHandler` -- `createSearchHandler`, `createSearchAdminHandler`, `createSearchStatusHandler` -- `createSparqlValidationHandler`, `createSparqlDryRunHandler`, - `createSparqlQueryHandler` -- `createWorkshopMcpHandler`, `createWorkshopHealthHandler`, - `createWorkshopProbeHandler` - -Handlers accept Web `Request` and return Web `Response`, making them suitable for -thin host-framework shims. JSON routes are same-origin by default, -enforce request limits and capabilities, return `{ error: WorkshopErrorBody }`, -and never expose stacks. Health is safe and public. Probe diagnostics (`GET`) -require exact `admin`; the mutating semantic probe (`POST`) requires exact -`admin`, `task-write`, and `memory-write`. -Task archival is revision-conditional: HTTP clients send the task's exact -`revision` value in `X-Workshop-Revision` (preferred) or the legacy exact -`updatedAt` value in `If-Match`. Completed tasks are terminal and cannot later -be archived; the semantic probe uses a separate disposable archive task. - -`createWorkshopClient` from `/protocol` defaults to same-origin -`/api/workshop/*`. It supports a base URL, injectable `fetch`, asynchronous token -callback, `AbortSignal`, and typed `WorkshopError`. It imports no server module -and accesses no browser global until the client is constructed. The typed -`tasks.archive(id, revisionOrUpdatedAt)` supplies the matching revision header -automatically. - -`tasks.history(id, { limit })`, `memories.history(slug, { limit })`, and -`prompts.history(id, { limit })` are required public client operations. Their -GET routes accept the same optional positive `limit` query parameter, bounded -by Workshop's configured maximum page size, and return newest-first revision -arrays with live authorization rechecked immediately before response creation. diff --git a/docs/limits.md b/docs/limits.md deleted file mode 100644 index 390bf7e..0000000 --- a/docs/limits.md +++ /dev/null @@ -1,43 +0,0 @@ -# Operational limits - -Default limits are exported as `defaultWorkshopLimits` and may be lowered by a -Site: - -| Limit | Default | -| ------------------------- | ----------------------: | -| Description | 4 KiB | -| Prompt | 128 KiB | -| Result | 256 KiB | -| Memory content | 128 KiB | -| Linked memories per task | 100 | -| Context queries per task | 20 | -| Query | 16 KiB | -| MCP/JSON request | 1 MiB | -| Tool timeout | 30 seconds | -| Page size | 50 default, 200 maximum | -| Cursor candidate snapshot | 1,000 entries | -| Cursor token | 8 KiB | -| Cursor snapshot lifetime | 15 minutes | -| Expired snapshot cleanup | 100 per new snapshot | - -Limits return structured errors. A query with more than 1,000 visible -candidates fails generically instead of truncating or disclosing a count. -Cursor snapshots contain only IDs, revisions, ordering timestamps, and binding -digests; they never contain Task or Memory text. Stored context queries are -statically validated but are not executed. - -## Local regression measurements - -`npm run performance:check` measures the built package against Miniflare D1 and -fails any common operation above a deliberately generous two-second regression -bound. On 2026-07-21 the full remediation gate measured Task search at 486.27 -ms, a one-query packet at 309.45 ms, MCP initialization at 1.79 ms, `tools/list` -at 0.31 ms, and a Knowledge-service call at 0.18 ms. Compiled UI modules were -42,828 bytes raw / 7,745 bytes gzip. The isolated Workshop Worker package was -689.23 KiB raw / 131.15 KiB gzip and initialized in 37.4 ms in local Miniflare. -These are development-machine package regression baselines, not complete-Site -or production latency claims. -Workshop does not cache claims. Operators should observe packet duration, task -search, MCP initialize/list, knowledge calls, runtime startup, and UI bundle size -before raising limits. The Site-creating agent owns deployed performance -acceptance. diff --git a/docs/mcp.md b/docs/mcp.md deleted file mode 100644 index f28dd0b..0000000 --- a/docs/mcp.md +++ /dev/null @@ -1,57 +0,0 @@ -# MCP tool reference - -`createWorkshopMcpServer` implements stateless Streamable HTTP for MCP -`2025-11-25` and returns JSON responses. POST clients must accept -`application/json` and `text/event-stream`; GET returns 405 because Workshop does -not open a server-notification SSE stream. No task state lives in transport -sessions. - -## Tasks - -`list_tasks`, `search_tasks`, `get_task_packet`, `create_task`, `update_task`, -`archive_task`, `claim_task`, `complete_task`, `task_history`. - -`update_task` and `archive_task` require `expectedRevision` (preferred) or the -legacy exact current `expectedUpdatedAt` -timestamp. This makes concurrent complete/archive transitions deterministic. - -`task_history` requires exact `read`, accepts an optional bounded `limit`, and -returns newest-first canonical revisions only after authorizing every snapshot -and rechecking the exact current Task and live authorization state. - -## Memories - -`list_memories`, `get_memory`, `upsert_memory`, `delete_memory`, -`memory_history`. - -`memory_history` has the same bounded newest-first, per-snapshot authorization, -canonical-identity, and final current-record recheck guarantees as Task history. - -## Prompts and unified search - -`list_prompts`, `get_prompt`, `create_prompt`, `update_prompt`, `delete_prompt`, -`prompt_history`, `search`, `search_status`, `search_admin`. - -`prompt_history` uses the same hardened history read boundary. - -Prompt, Memory, and Task writes are sealed atomically with their canonical -Taproot source events. `search` uses Taproot's authorization-aware result and -hydration boundary. `search_admin` dispatches bounded legacy adoption, -materialization/rebuild/retry, and semantic lifecycle actions; it requires -`search:admin`. Semantic provider and vector-port configuration is a host API -because runtime secrets and capability objects are not JSON tools. - -## Knowledge - -`search_entities`, `get_entity`, `get_entities`, `export_entity_json`. - -Results come from Taproot's `AuthorizedTaprootReader` and include -entity/revision metadata. Knowledge mutations and SPARQL execution are not -registered or advertised; direct mutation calls fail closed. `tools/list` is -deterministic and capability-filtered. Domain failures are `isError` tool -results so agents can correct inputs. Unknown tools remain JSON-RPC errors. -Administrative claim reset is intentionally absent. - -Task context queries remain stored, statically validated metadata. Packet -hydration returns a bounded per-query forbidden result instead of executing -them until a scoped Diamond boundary exists. diff --git a/docs/migrations.md b/docs/migrations.md deleted file mode 100644 index a4037e7..0000000 --- a/docs/migrations.md +++ /dev/null @@ -1,44 +0,0 @@ -# Migrations and upgrade policy - -Workshop owns six exact migrations: `0001_workshop` creates tasks and -memories, `0002_revisions` adds monotonic concurrency tokens, and -`0003_resumable_onboarding` adds durable run and step checkpoints. -`0004_authorization` adds installation, owner, workspace, canonical visibility, -and authorization-revision fields plus candidate-selection indexes. -`0005_cursor_snapshots` adds durable bounded cursor snapshots, metadata-only -entries, expiry cleanup support, and cascading entry deletion. -`0006_canonical_search_domains` adds canonical Task/Memory metadata and event -references, immutable revision snapshots, Prompt storage/history, and Prompt -cursor support. Every entry -has a monotonic ID and SHA-256 in `workshopMigrations`; the same canonical SQL -ships under `migrations/`. - -Consumers may materialize the manifest through their chosen installer or call -`applyWorkshopMigrations(persistence)` explicitly. Runtime construction never -discovers SQL from disk or creates tables. Health compares `workshop_schema`, -required tables, and indexes through the neutral persistence capability. - -The runner uses Diamond 0.4's shared `_gnolith_migrations` ledger under the -`@gnolith/workshop` namespace. Every migration is committed atomically with its -Diamond-computed checksum. Unknown IDs, checksum drift, partial histories, and -newer versions fail closed. A pre-ledger Workshop database is adopted only after -Workshop verifies the tables and revision columns required by its recorded -schema version. Version 4 validation includes the exact authorization table -constraints and composite Memory key; version 5 validates the exact cursor -tables, foreign key, and expiry index. Weakened near-miss schemas and schema -metadata drift are rejected without recording ledger adoption. Version 6 also -validates Prompt/revision tables and the expanded cursor source-kind contract. - -Pre-v4 Task and Memory rows remain deliberately quarantined with null metadata. -The host may inspect readiness and run an explicit bounded, resumable backfill -through the server API with exact `search:admin`. The host executes each batch -behind a domain-specific Taproot maintenance guard; authorization changes at the -fence leave all selected metadata untouched. Nothing auto-assigns legacy content -to an installation or workspace. - -Ordinary process commands must not silently migrate an existing database. -Seedbed owns migration ordering, lifecycle, backup, and recovery. Patch releases -do not make destructive schema changes. Additive upgrades use a new monotonic -file, update the manifest/schema version, and require clean and previous-version -upgrade tests. Destructive changes require explicit migration and compatibility -notes. diff --git a/docs/package-handoff.md b/docs/package-handoff.md deleted file mode 100644 index 9e9143b..0000000 --- a/docs/package-handoff.md +++ /dev/null @@ -1,35 +0,0 @@ -# Package handoff - -## Workshop package handoff ready - -This phase is complete when the exact npm tarball passes the local release gate -and provides the documented protocol, server, MCP, Site route, UI, migration, -health, and observability surfaces. The package owns runtime -boundaries, schemas, validation, tests, assets, and consumer compatibility. -The public process/core and browser-client composition contracts include -required bounded Task and Memory history operations; hosts only route and -authenticate those package-owned services. - -The downstream Site supplies D1, Diamond health, Taproot persistence and its -authorized-reader boundary, the shared live authorization source with separate -host-issued Task, Memory, authorization-maintenance, and cursor-snapshot domain -guards, opaque cursor cryptography/generation state, identity/capabilities, Waystone -registration, deployment configuration, and any browser endpoints used by the -MCP-status controller. Workshop runtime construction never provisions -infrastructure or applies migrations. - -Workshop CI packs released Taproot 0.4.0 source at -`819fe054ebb867e1ca92518bfd3b1aa6c5aa277d` and verifies the typed boundary plus -real native SQLite and persisted D1 runtime behavior. Passing that package gate -still does not assemble or accept a complete Gnolith Site. - -## Ownership boundary - -Workshop may install its exact tarball in isolated Worker, vinext, Miniflare, -and generic consumers with injected or stubbed peer services. These checks -verify the package's exports, runtime boundaries, and supported-consumer -portability. They neither provision infrastructure nor qualify a complete Site. - -The Codex agent creating a Site owns four-package assembly, Site migrations and -bindings, host identity/secrets/configuration, deployment, live probes, and final -acceptance. Site-specific host assets do not belong in the Workshop package. diff --git a/docs/peer-artifacts-v1.json b/docs/peer-artifacts-v1.json new file mode 100644 index 0000000..ff57cce --- /dev/null +++ b/docs/peer-artifacts-v1.json @@ -0,0 +1,50 @@ +{ + "format": "gnolith-workshop-peer-artifacts-v1", + "version": 1, + "peers": [ + { + "name": "diamond", + "destinationDirectory": "diamond", + "repository": "https://github.com/gnolith/diamond.git", + "commit": "e82f58ba8028ff9c16a34f4b8a9a2bc356495b64", + "tree": "a78bc6ccceda38a3a3db7b4cc8e4bd686e945a06", + "filename": "gnolith-diamond-0.5.0.tgz", + "bytes": 71762, + "sha256": "3ce5d33478667dc4ec9a8ae4b4c3e0a94c3bce61e741ba62511ab668d1c20228", + "integrity": "sha512-KrgOpBB640UoNjFMiDqN8DgB4VYBDbiKRtZ4bKKR4kETN3QzPU9hUBEyNXZsyLA2IbE4YlQka/GflrsP7aNU8g==" + }, + { + "name": "taproot", + "destinationDirectory": "taproot-artifacts", + "repository": "https://github.com/gnolith/taproot.git", + "commit": "98c7bc6e66b39d4bba0862a7e5dcb3a58a7882ac", + "tree": "b3f11979405f1ecfca881367ce1ee40218d83d71", + "filename": "gnolith-taproot-0.5.0.tgz", + "bytes": 348272, + "sha1": "6a1e6d54293d57067ee37e3a3aae447e04127023", + "sha256": "030e0bc4b58873b1ba362cc7ea0a232b769fcd5163ce7679d397c47b8f15b818", + "integrity": "sha512-uVA0FBUyQA3SwNGtxk93mHeoZibzzddHvbmSJ8krZKWekLbHSGNDtYBCmtPTpMZPhlrWMJYYBO+7NjpFy1Tkbg==", + "canonicalEvidence": { + "hostedRunId": "30130270744", + "hostedArtifactId": "8610844408", + "hostedArtifactName": "taproot-release-candidate-98c7bc6e66b39d4bba0862a7e5dcb3a58a7882ac", + "expiresAt": "2026-10-22", + "sbom": { + "filename": "gnolith-taproot-0.5.0.cdx.json", + "bytes": 734126, + "sha256": "cee47a765e917037ce0c3d0c28e481d0a48c204f729e9a28dff730df6bf5f3fc" + }, + "provenance": { + "filename": "gnolith-taproot-0.5.0.provenance.json", + "bytes": 1150, + "sha256": "678f76bbef3433ba9b7f206052af68575033334c05c28cc7413d6fb68f8915fa" + }, + "inventory": { + "filename": "SHA256SUMS", + "bytes": 293, + "sha256": "116e33d84b5bf8578e46d8c106cf3c4ffa33b8346368d504c6114e6a532e189b" + } + } + } + ] +} diff --git a/docs/release-checklist.md b/docs/release-checklist.md deleted file mode 100644 index 8da02b3..0000000 --- a/docs/release-checklist.md +++ /dev/null @@ -1,41 +0,0 @@ -# Release checklist - -> **Publication is package-gated.** `release:check` and normal `npm publish` -> require public, provenance-verified `@gnolith/taproot` 0.4.0 plus Workshop's -> clean, tagged, version-consistent artifact provenance. Complete-Site -> acceptance is neither a prerequisite nor evidence for a Workshop release. - -## Workshop package handoff - -1. Confirm the version, peer ranges, changelog, license, and public exports. -2. Run `npm ci && npm run check` on supported Node releases. - This is the package-owned full gate, including security, native SQLite, - persisted D1, and exact packed-peer conformance. -3. Verify migration order, checksums, clean application, and upgrade behavior. -4. Run `npm run artifact:prepare` once; retain its generated commit, environment, - SHA-256, npm integrity, and file-manifest provenance. -5. Install that existing artifact in generic and isolated Worker/vinext - consumers with injected or stubbed peers, then run `artifact:verify`. -6. Confirm the isolated Workshop package consumer needs no undeclared runtime - compatibility flags. -7. Audit MCP tool descriptions/schemas, browser/server dependency boundaries, - health/telemetry safety, documentation, and package contents. -8. Commit the final tree and create the annotated `v0.4.2` tag with explicit - authorization, then regenerate provenance from that clean tagged checkout. -9. Run `npm run release:check -- v0.4.2`; it must verify exact archive contents, - source identity, version uniqueness, the clean commit/tree, and annotated tag - without repacking. Normal `npm publish` derives this tag from the package - version and runs both the Taproot provenance gate and the same Workshop - release check before publication. -10. Push the annotated tag. The tag-driven OIDC workflow must repeat package - verification, publish the verified archive rather than the package - directory, verify exact npm provenance, and only then create the immutable - GitHub Release with provenance assets. Do not create the Release manually. - -## Excluded from Workshop release acceptance - -Workshop release acceptance excludes complete Gnolith Site composition, -infrastructure provisioning, Site migration application, host identity and -secrets, deployment configuration, live HTTP/browser/MCP/Codex probes, and final -Site approval. The Codex agent creating the Site owns those checks. -Package CI must not provision, deploy, assemble, or accept a complete Site. diff --git a/docs/release-evidence.md b/docs/release-evidence.md deleted file mode 100644 index ec2861c..0000000 --- a/docs/release-evidence.md +++ /dev/null @@ -1,78 +0,0 @@ -# Workshop 0.4.2 release evidence - -## Workshop package handoff ready - -Current local package decision: **PACKAGE GATES PASS; TAGGED RELEASE EVIDENCE -PENDING**. -Workshop 0.4.2 requires public `@gnolith/taproot` 0.4.0 with exact npm -integrity `sha512-yYxbrUNnu74zBaxHoywGlgeG2LFz4HMzi2RLcsq83/JVEIkwbWvWZ8tuLpxFYxTAgTXG1/FHddgEJStRupe54A==` -and npm SLSA provenance resolving annotated `v0.4.0` to source commit -`819fe054ebb867e1ca92518bfd3b1aa6c5aa277d`. A clean matching -annotated Workshop tag and regenerated artifact provenance remain required. -Normal `npm publish` derives the matching tag from the package version and runs -both the Taproot provenance gate and Workshop's clean/tagged provenance check. -This is not authorization to tag or publish. - -The trusted tag-driven workflow creates the immutable GitHub Release only after -the verified archive is public through npm OIDC and exact npm provenance has -been revalidated. A manually pre-created Release is not a valid trigger. - -The immutable GitHub `v0.3.2` tag and Release are historical only. The OIDC -workflow stopped before npm publication, so `@gnolith/workshop@0.3.2` and its -npm provenance do not exist and must not be consumed. - -The current verification environment was Microsoft Windows NT 10.0.26200.0, -Node.js v24.14.0, npm 11.9.0, and Git 2.45.1.windows.1. System Git supplied -`core.autocrlf=true`; the repository `.gitattributes` policy resolved tracked -text to `eol: lf`, and the normal `npm run format:check` command passed. CI also -defines a bounded Windows regression that sets `core.autocrlf=true` before -checkout and runs the same format command after `npm ci`. - -| Package-owned gate | Evidence | Status | -| ---------------------------------------------- | ----------------------------------------------------------------------------- | ---------------- | -| Format, lint, strict types, tests, coverage | Full current `npm run check`; exact totals recorded by the command | PASS | -| D1 migration, schema, task/memory/prompt races | Miniflare clean-schema and integration tests | PASS | -| Embedded SQLite parity | Exact Diamond/Workshop tarballs; reopen, migrations, health, contention | PASS | -| Released Taproot runtime boundary | Exact 819fe packed peer; Task/Memory/Prompt typed and runtime lanes | PASS | -| Context-query and Taproot reader boundary | Static parser, fail-closed packets, authorized-reader tests | PASS | -| MCP schemas and official client | Tool inventory plus SDK 1.29 initialize/list/call | PASS | -| Route factories, auth, health, observability | Unit and isolated package-runtime lifecycle tests | PASS | -| Waystone UI | UI tests plus public Waystone 0.2.0 normal-install/declaration/registry probe | PASS | -| Isolated Worker package consumer | 742.64 KiB / 139.85 KiB gzip; 40.82 ms cold initialize | PASS | -| Exact npm tarball consumers | One prepared artifact; generic, Worker, and vinext isolated consumers | PASS | -| Artifact provenance | Schema, commit/worktree, hashes, parsed tar manifest/exports/migrations | PASS | -| Dev-tool compatibility and full audit | Hono Node HTTP bridge; Sharp PNG-to-WebP; exactly zero vulnerabilities | PASS | -| Performance, production audit, readiness | Included in `npm run check` | PASS | -| Tagged release invariant | Requires clean matching annotated tag; current clean/no-tag head rejected | EXPECTED BLOCKED | -| Complete-Site acceptance | Assembly, provisioning, deployment, and acceptance are host-agent owned | OUT OF SCOPE | - -The final command output is the source of truth if this document and a later -working-tree state differ. - -The machine-verifiable record beside the ignored archive under `.release/` is -the source of truth for the exact commit/worktree state, environment, artifact -SHA-256 and npm integrity, and sorted file manifest. Its `dirty`, commit, and tag -fields are authoritative for the state in which it was generated. It must be -regenerated after the final commit and annotated tag before publication. The -publish workflow retains the provenance and versioned schema as non-replaceable -GitHub Release assets. - -Final performance and artifact measurements must be copied from the clean, -tagged 0.4.2 gate. Historical measurements are not release evidence for this -version. - -## Evidence boundary - -The Worker, vinext, and Miniflare results are isolated package-runtime consumers -with injected or stubbed peer services. This evidence does not qualify a -complete Gnolith Site. - -The vinext SSR build passed but warned about Node imports originating in vinext -and React DOM. That counterevidence remains a full-Site compatibility item; the -isolated package check is not evidence that a deployed Site needs no additional -runtime configuration. - -The Codex agent creating a Site owns complete package assembly, managed bindings and -migrations, host identity and secrets, deployment configuration, real graph -projection, live Waystone/browser/MCP/Codex probes, and final acceptance. -Workshop CI must not provision, deploy, assemble, or accept that complete Site. diff --git a/docs/release-provenance.md b/docs/release-provenance.md deleted file mode 100644 index 285fd30..0000000 --- a/docs/release-provenance.md +++ /dev/null @@ -1,40 +0,0 @@ -# Release artifact provenance - -Workshop creates one package archive for local package qualification: - -```sh -npm run artifact:prepare -npm run consumer:check -npm run artifact:verify -``` - -`artifact:prepare` writes an ignored `.release/gnolith-workshop-.tgz` -and adjacent `.provenance.json`. The JSON conforms to -`release-provenance.schema.json` and records: - -- package name and version; -- source commit, tree, dirty state, and a worktree-state SHA-256; -- OS, architecture, Node, npm, Git, and `core.autocrlf` values; -- artifact filename, size, SHA-256, npm SHA-1 shasum, SHA-512 integrity, and the - sorted npm file manifest. - -The generic, Worker, and vinext consumers install that existing archive. They do -not repack the working directory. `artifact:verify` parses the retained archive, -validates the provenance against the JSON Schema, compares the actual tar -manifest, validates every export target, and checks packed migration bytes and -checksums against the packed compiled manifest. - -`release:check` is stricter than local verification. It requires a clean source -state, provenance commit/tree equal to the current `HEAD`, and an existing -annotated version tag that resolves to that commit. It is expected to fail in an -uncommitted review tree. An annotated version-tag push runs the trusted workflow, -publishes the same verified `.tgz` through npm OIDC, and revalidates its exact -registry provenance. Only after those checks succeed does the workflow create -the immutable GitHub Release with the provenance and versioned schema assets. -A manually pre-created Release is rejected rather than used as a trigger. - -The version-history check compares package-producing inputs with every reachable -commit that declared the same version. Material package changes require a new -version or prerelease identifier. A dirty local verification is recorded as -such. After the final commit and annotated tag exist, regenerate the artifact so -release provenance names the clean tagged commit before publishing. diff --git a/docs/release-provenance.schema.json b/docs/release-provenance.schema.json deleted file mode 100644 index 2de9b60..0000000 --- a/docs/release-provenance.schema.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/gnolith/workshop/blob/main/docs/release-provenance.schema.json", - "title": "Workshop release artifact provenance", - "type": "object", - "required": [ - "schemaVersion", - "generatedAt", - "package", - "source", - "environment", - "artifact" - ], - "properties": { - "schemaVersion": { "const": 1 }, - "generatedAt": { "type": "string", "format": "date-time" }, - "package": { - "type": "object", - "required": ["name", "version"], - "properties": { - "name": { "const": "@gnolith/workshop" }, - "version": { - "type": "string", - "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?$" - } - }, - "additionalProperties": false - }, - "source": { - "type": "object", - "required": ["commit", "tree", "dirty", "stateSha256"], - "properties": { - "commit": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, - "tree": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, - "dirty": { "type": "boolean" }, - "stateSha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" } - }, - "additionalProperties": false - }, - "environment": { - "type": "object", - "required": [ - "platform", - "release", - "version", - "arch", - "node", - "npm", - "git", - "coreAutocrlf" - ], - "properties": { - "platform": { "type": "string", "minLength": 1 }, - "release": { "type": "string", "minLength": 1 }, - "version": { "type": "string", "minLength": 1 }, - "arch": { "type": "string", "minLength": 1 }, - "node": { "type": "string", "pattern": "^v[0-9]+\\." }, - "npm": { "type": "string", "minLength": 1 }, - "git": { "type": "string", "pattern": "^git version " }, - "coreAutocrlf": { "type": ["string", "null"] } - }, - "additionalProperties": false - }, - "artifact": { - "type": "object", - "required": [ - "filename", - "size", - "sha256", - "shasum", - "integrity", - "files" - ], - "properties": { - "filename": { - "type": "string", - "pattern": "^gnolith-workshop-[0-9A-Za-z.-]+\\.tgz$" - }, - "size": { "type": "integer", "minimum": 1 }, - "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, - "shasum": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, - "integrity": { - "type": "string", - "pattern": "^sha512-[A-Za-z0-9+/]+={0,2}$" - }, - "files": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": ["path", "size", "mode"], - "properties": { - "path": { "type": "string", "minLength": 1 }, - "size": { "type": "integer", "minimum": 0 }, - "mode": { "type": "integer", "minimum": 0, "maximum": 511 } - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false -} diff --git a/docs/security.md b/docs/security.md deleted file mode 100644 index 7efd8c4..0000000 --- a/docs/security.md +++ /dev/null @@ -1,56 +0,0 @@ -# Security considerations and threat assumptions - -Workshop assumes the public Internet, hostile prompts/tool inputs, concurrent -agents, stale revisions, and compromised browser origins. It does not assume a -trusted MCP client merely because a URL is known. - -- Hosts authenticate every request and inject Taproot's shared live - authorization authority. Every Task, Memory, Packet, and Knowledge boundary - requires a full authorization context. -- Same-origin is the default; explicit CORS and MCP Origin validation prevent - DNS-rebinding/cross-origin access. -- D1 values use bound statements. Conditional writes enforce concurrency. -- Candidate Task/Memory reads are authorization-filtered in SQL before content - hydration and rechecked afterward. Missing, cross-installation, and denied - records share one non-disclosing denial. -- Unscoped public SPARQL execution fails closed. Entity search is available only - through Taproot's authorized-reader candidate and hydration boundary. -- Knowledge writes are unavailable and unadvertised until shared Taproot policy - persistence and atomic mutation conformance are complete. -- Request/content/query/page/tool limits reduce resource exhaustion. -- Public errors omit stacks and internals. Observability hooks must never log - tokens, secrets, entire packets, sensitive source bodies, or arbitrary MCP - request bodies. -- Detailed health requires exact `admin`. Mutating semantic probes require - exact `admin`, `task-write`, and `memory-write`; claim reset requires exact - `admin` and `task-write`. Legacy authorization backfill requires exact - `search:admin`. Generic `admin` never implies another capability. -- React renders prompts/results/memories as text; no raw HTML path is provided. - -Workshop's package boundary assumes the consumer supplies correct principals, -protects bearer tokens and database handles, and configures intentional origins. -Run the full `npm run security:dev-check`, `npm audit --omit=dev`, dependency -review, code scanning, and focused authorization/context-query/MCP review before -every public package release. - -## 0.3 development-tool compatibility record - -Workshop pins transitive development tools that participate in package-runtime -qualification to `sharp@0.35.3` and `@hono/node-server@2.0.11`. The gate imports, -constructs, and closes the MCP SDK's Node Streamable HTTP transport, resolves the -same native Sharp installation used from Miniflare's dependency context, and -performs a PNG-to-WebP conversion before requiring a zero-vulnerability full -audit. Hono 2.0.5 fixed the original Windows static-file path traversal but was -rejected because a later WebSocket-abort denial-of-service advisory affects -2.0.0 through 2.0.9. - -## 0.1 focused review record - -The 2026-07-20 local review verified server-side capability checks, same-origin -CORS defaults, prepared D1 statements, bounded streaming request reads, bounded -static context-query validation, structured non-stack errors, safe React text rendering, no -administrative reset MCP tool, telemetry that cannot alter domain outcomes, -clean production dependency audit, isolated Worker package-consumer build, and -exact tarball contents. The Codex agent creating a Site owns live binding scope, -edge rate limits, real Taproot policy/projection, deployed authentication, -deployment, and complete-Site security acceptance. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md deleted file mode 100644 index ffdd1c2..0000000 --- a/docs/troubleshooting.md +++ /dev/null @@ -1,17 +0,0 @@ -# Troubleshooting - -- `schema` health false: materialize/apply the migration manifest; runtime does - not create tables. Compare admin diagnostics with the manifest. -- task creation rejects a query: inspect structured `queryIndex`/`label`, use - `validate_sparql`, then `dry_run_sparql`; remove updates or unapproved SERVICE. -- claim returns conflict: another caller won, or the task is completed/archived. -- task update returns conflict: reload and use the latest `updatedAt`. -- knowledge write returns conflict: reload canonical entity JSON and use the - latest Taproot revision. -- MCP returns 406: POST clients must accept JSON and event-stream. -- MCP returns 401/403: configure machine authentication and capabilities; task - role labels do not help. -- browser call is blocked: same-origin is default; configure one intentional - allowed origin in the runtime. -- consumer bundle imports an unexpected runtime: confirm it imports the correct - explicit subpath and does not pull test/release scripts into runtime code. diff --git a/docs/ui-and-onboarding.md b/docs/ui-and-onboarding.md deleted file mode 100644 index ed0d193..0000000 --- a/docs/ui-and-onboarding.md +++ /dev/null @@ -1,33 +0,0 @@ -# Waystone UI - -Use `createWorkshopPlugin(options)` and register its result through Waystone's -public structural plugin contract. Import `@gnolith/workshop/styles.css` once -in the consumer shell. The default `workshopPlugin` uses a same-origin, -read-only client; hosts normally inject a client factory and the current -capability set. Workshop does not import Waystone internals, server code, or D1 -from browser code. - -The plugin contributes Tasks and Memories navigation, routes, a dashboard, -related-task entity panel, and admin MCP status. Route components load and -filter tasks and memories, create and edit them, compile packet views, and claim -or complete tasks. Loading, denied permissions, request failures, and revision -conflicts are explicit. The client and status loader are host-injected browser -contracts; no UI module constructs a Workshop server runtime. - -```tsx -const plugin = createWorkshopPlugin({ - client: () => createWorkshopClient({ token: getSessionToken }), - capabilities, - loadMcpStatus: () => statusApi.workshopMcp(), -}); -``` - -Exported components cover lists, filters, cards, state badges, detail, packets, -query-result status, claim/completion controls, editors, and MCP status. -Content is rendered as text, actions use semantic controls, permission/conflict -errors are explicit, and layouts are responsive. - -Onboarding is intentionally absent from the public plugin, protocol, core, -HTTP, and MCP surfaces. Historical journal tables remain migration artifacts -only. Future repeatable research exploration belongs in the separately scoped -agent-skill work, after the shared Taproot authorization foundation is complete. diff --git a/docs/verification.md b/docs/verification.md deleted file mode 100644 index 8f73a58..0000000 --- a/docs/verification.md +++ /dev/null @@ -1,18 +0,0 @@ -# Health and semantic verification - -Public health verifies D1, Workshop schema, Taproot, Diamond, MCP construction, -and compatibility but returns only safe booleans, versions, and overall status. -Admin diagnostics add table/index presence; they never return SQL, binding IDs, -secrets, auth configuration, or stacks. - -The local protected probe handler exercises memory creation, validated task -creation, packet resolution, claim, completion, and archive against injected -services. It is package behavior, not a deployed Site acceptance probe. - -The package MCP gate initializes, lists required tools, performs harmless reads -and temporary task/knowledge calls, rejects anonymous and insufficient calls, -and uses the official MCP client locally. - -The Codex agent creating a Site owns real Taproot/Diamond projection, deployed -MCP, browser behavior, the intended Codex connection, cleanup policy, and live -acceptance. See `release-evidence.md` for the package evidence boundary. diff --git a/eslint.config.js b/eslint.config.js index 5aed1c2..da327cd 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,22 +1,24 @@ import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; -import reactHooks from 'eslint-plugin-react-hooks'; export default tseslint.config( + { ignores: ['dist/**', 'src/generated/**'] }, eslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, - reactHooks.configs.flat.recommended, { - ignores: ['dist/**'], languageOptions: { parserOptions: { projectService: { allowDefaultProject: ['scripts/*.mjs'], - maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 17, + maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 32, }, tsconfigRootDir: import.meta.dirname, }, }, + rules: { + 'no-control-regex': 'off', + '@typescript-eslint/prefer-promise-reject-errors': 'off', + }, }, { files: ['test/**/*.{ts,tsx}'], diff --git a/examples/package-runtime-canary/README.md b/examples/package-runtime-canary/README.md deleted file mode 100644 index 11f010a..0000000 --- a/examples/package-runtime-canary/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Isolated package-runtime canary - -This fixture verifies that the exact published Workshop package bundles in a -supported Worker runtime. It is invoked only through Wrangler's local -`deploy --dry-run` output and Miniflare. - -Diamond, Taproot, identity, and host services are injected or stubbed. The D1 -identifier is intentionally invalid. This directory does not assemble, -provision, deploy, or accept a complete Gnolith Site, and it is not included in -the Workshop npm package. - -The Codex agent creating a Site owns deployable host assets, four-package -composition, infrastructure, live probes, and final acceptance. diff --git a/examples/package-runtime-canary/worker.ts b/examples/package-runtime-canary/worker.ts deleted file mode 100644 index 1a26b04..0000000 --- a/examples/package-runtime-canary/worker.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { createWorkshopRuntime } from '@gnolith/workshop/server'; -import { - createWorkshopHealthHandler, - createWorkshopMcpHandler, -} from '@gnolith/workshop/site'; - -// Isolated package-runtime fixture only. Peer services are deliberately stubbed; -// this module is not a deployable Gnolith Site entry point. - -interface Env { - DB: import('@gnolith/workshop/server').D1DatabaseLike; - CANARY_TOKEN: string; -} - -export default { - async fetch(request: Request, env: Env): Promise { - const runtime = createWorkshopRuntime({ - db: env.DB, - authorization: { - async getInstallationAuthorizationState() { - return { - installationId: 'canary:installation', - authorizationRevision: 1, - searchGeneration: 1, - }; - }, - async commitTaskMutation(_db, _context, mutation) { - return mutation.first(); - }, - async commitMemoryMutation(_db, _context, mutation) { - return mutation.first(); - }, - async commitTaskBackfill(db, _context, _state, mutations) { - return db.batch([...mutations]); - }, - async commitMemoryBackfill(db, _context, _state, mutations) { - return db.batch([...mutations]); - }, - async commitCursorSnapshot(db, _context, _state, mutations) { - return db.batch([...mutations]); - }, - }, - knowledge: { - authorizedReader() { - return { - async getEntity() { - return null; - }, - async searchEntities() { - return { items: [], cursor: null }; - }, - }; - }, - health: async () => false, - }, - diamondHealth: async () => false, - cursorCodec: { - currentGeneration: async () => 'canary-generation', - async digest(purpose, value) { - const key = await crypto.subtle.importKey( - 'raw', - new TextEncoder().encode(env.CANARY_TOKEN), - { name: 'HMAC', hash: 'SHA-256' }, - false, - ['sign'], - ); - const signature = await crypto.subtle.sign( - 'HMAC', - key, - new Uint8Array([...new TextEncoder().encode(purpose), 0, ...value]), - ); - return Array.from(new Uint8Array(signature), (byte) => - byte.toString(16).padStart(2, '0'), - ).join(''); - }, - async seal() { - throw new Error('Pagination is outside this package canary'); - }, - async open() { - throw new Error('Pagination is outside this package canary'); - }, - }, - async resolvePrincipal(candidate) { - if ( - candidate.headers.get('authorization') !== - `Bearer ${env.CANARY_TOKEN}` - ) - return null; - return { - installationId: 'canary:installation', - principalId: 'canary:verifier', - activeWorkspaceId: 'canary:workspace', - workspaceIds: ['canary:workspace'], - capabilities: [ - 'read', - 'task-write', - 'memory-write', - 'knowledge-write', - 'admin', - ], - authorizationRevision: 1, - }; - }, - }); - const path = new URL(request.url).pathname; - if (path === '/health') - return createWorkshopHealthHandler(runtime)(request); - if (path === '/mcp') return createWorkshopMcpHandler(runtime)(request); - return new Response('Not found', { status: 404 }); - }, -}; diff --git a/examples/package-runtime-canary/wrangler.jsonc b/examples/package-runtime-canary/wrangler.jsonc deleted file mode 100644 index c67663d..0000000 --- a/examples/package-runtime-canary/wrangler.jsonc +++ /dev/null @@ -1,14 +0,0 @@ -{ - // Non-deployable fixture used only with `wrangler deploy --dry-run`. - "$schema": "node_modules/wrangler/config-schema.json", - "name": "gnolith-workshop-package-runtime-canary", - "main": "worker.ts", - "compatibility_date": "2026-07-20", - "d1_databases": [ - { - "binding": "DB", - "database_name": "gnolith-workshop-package-runtime-canary", - "database_id": "00000000-0000-0000-0000-000000000000", - }, - ], -} diff --git a/migrations/0007_task_leases.sql b/migrations/0007_task_leases.sql new file mode 100644 index 0000000..c62ffbb --- /dev/null +++ b/migrations/0007_task_leases.sql @@ -0,0 +1,42 @@ +ALTER TABLE workshop_tasks ADD COLUMN lease_id TEXT; +ALTER TABLE workshop_tasks ADD COLUMN lease_token_digest TEXT; +ALTER TABLE workshop_tasks ADD COLUMN lease_owner_principal_id TEXT; +ALTER TABLE workshop_tasks ADD COLUMN lease_expires_at TEXT; + +CREATE TABLE workshop_task_lease_receipts ( + installation_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + idempotency_key TEXT NOT NULL, + task_id TEXT NOT NULL, + operation TEXT NOT NULL CHECK (operation IN ('release', 'complete')), + expected_revision INTEGER NOT NULL CHECK (expected_revision >= 1), + resulting_revision INTEGER NOT NULL CHECK (resulting_revision >= 1), + receipt_json TEXT NOT NULL CHECK (json_valid(receipt_json)), + created_at TEXT NOT NULL, + PRIMARY KEY (installation_id, principal_id, idempotency_key) +); + +CREATE TABLE workshop_task_lease_events ( + event_id TEXT PRIMARY KEY, + installation_id TEXT NOT NULL, + task_id TEXT NOT NULL, + lease_id TEXT NOT NULL, + actor_principal_id TEXT NOT NULL, + event_kind TEXT NOT NULL CHECK ( + event_kind IN ('claimed', 'reclaimed_expired', 'released', 'completed') + ), + task_revision INTEGER NOT NULL CHECK (task_revision >= 1), + lease_expires_at TEXT, + created_at TEXT NOT NULL +); + +CREATE INDEX workshop_tasks_lease_expiry_idx + ON workshop_tasks (installation_id, lease_expires_at, id); +CREATE INDEX workshop_task_lease_events_task_idx + ON workshop_task_lease_events (installation_id, task_id, created_at); + +UPDATE workshop_schema +SET version = 7, + package_version = '0.5.0', + updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') +WHERE singleton = 1; diff --git a/migrations/0008_bootstrap_credentials.sql b/migrations/0008_bootstrap_credentials.sql new file mode 100644 index 0000000..cc1375d --- /dev/null +++ b/migrations/0008_bootstrap_credentials.sql @@ -0,0 +1,40 @@ +CREATE TABLE workshop_credentials ( + installation_id TEXT NOT NULL, + credential_id TEXT NOT NULL, + profile TEXT NOT NULL CHECK (profile = 'local-bearer-v1'), + token_digest TEXT NOT NULL CHECK ( + length(token_digest) = 64 AND token_digest NOT GLOB '*[^0-9a-f]*' + ), + principal_id TEXT NOT NULL, + workspace_id TEXT NOT NULL, + disabled INTEGER NOT NULL DEFAULT 0 CHECK (disabled IN (0, 1)), + created_at TEXT NOT NULL, + PRIMARY KEY (installation_id, credential_id), + UNIQUE (installation_id, token_digest) +); + +CREATE INDEX workshop_credentials_principal_idx + ON workshop_credentials (installation_id, principal_id, workspace_id); + +CREATE TABLE workshop_bootstrap_receipts ( + installation_id TEXT NOT NULL, + idempotency_key TEXT NOT NULL, + credential_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + workspace_id TEXT NOT NULL, + grant_manifest_digest TEXT NOT NULL CHECK ( + length(grant_manifest_digest) = 64 + AND grant_manifest_digest NOT GLOB '*[^0-9a-f]*' + ), + owner_receipt_json TEXT NOT NULL CHECK (json_valid(owner_receipt_json)), + created_at TEXT NOT NULL, + PRIMARY KEY (installation_id, idempotency_key), + FOREIGN KEY (installation_id, credential_id) + REFERENCES workshop_credentials(installation_id, credential_id) +); + +UPDATE workshop_schema +SET version = 8, + package_version = '0.5.0', + updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') +WHERE singleton = 1; diff --git a/migrations/0009_operation_receipts.sql b/migrations/0009_operation_receipts.sql new file mode 100644 index 0000000..2269d55 --- /dev/null +++ b/migrations/0009_operation_receipts.sql @@ -0,0 +1,40 @@ +CREATE TABLE workshop_operation_receipts ( + installation_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + operation TEXT NOT NULL CHECK ( + operation IN ( + 'memory.delete', + 'prompt.create', + 'prompt.update', + 'prompt.delete' + ) + ), + idempotency_key TEXT NOT NULL, + subject_id TEXT NOT NULL, + request_digest TEXT NOT NULL CHECK ( + length(request_digest) = 64 + AND request_digest NOT GLOB '*[^0-9a-f]*' + ), + response_json TEXT NOT NULL CHECK (json_valid(response_json)), + created_at TEXT NOT NULL, + PRIMARY KEY ( + installation_id, + principal_id, + operation, + idempotency_key + ) +); + +CREATE INDEX workshop_operation_receipts_subject_idx + ON workshop_operation_receipts ( + installation_id, + operation, + subject_id, + created_at + ); + +UPDATE workshop_schema +SET version = 9, + package_version = '0.5.0', + updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') +WHERE singleton = 1; diff --git a/migrations/0010_durable_mutation_receipts.sql b/migrations/0010_durable_mutation_receipts.sql new file mode 100644 index 0000000..39e6d99 --- /dev/null +++ b/migrations/0010_durable_mutation_receipts.sql @@ -0,0 +1,75 @@ +ALTER TABLE workshop_task_lease_receipts +ADD COLUMN request_digest TEXT CHECK ( + request_digest IS NULL OR ( + length(request_digest) = 64 + AND request_digest NOT GLOB '*[^0-9a-f]*' + ) +); + +ALTER TABLE workshop_operation_receipts +RENAME TO workshop_operation_receipts_v9; + +CREATE TABLE workshop_operation_receipts ( + installation_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + operation TEXT NOT NULL CHECK ( + operation IN ( + 'memory.upsert', + 'memory.delete', + 'prompt.create', + 'prompt.update', + 'prompt.delete' + ) + ), + idempotency_key TEXT NOT NULL, + subject_id TEXT NOT NULL, + request_digest TEXT NOT NULL CHECK ( + length(request_digest) = 64 + AND request_digest NOT GLOB '*[^0-9a-f]*' + ), + response_json TEXT NOT NULL CHECK (json_valid(response_json)), + created_at TEXT NOT NULL, + PRIMARY KEY ( + installation_id, + principal_id, + operation, + idempotency_key + ) +); + +INSERT INTO workshop_operation_receipts ( + installation_id, + principal_id, + operation, + idempotency_key, + subject_id, + request_digest, + response_json, + created_at +) +SELECT + installation_id, + principal_id, + operation, + idempotency_key, + subject_id, + request_digest, + response_json, + created_at +FROM workshop_operation_receipts_v9; + +DROP TABLE workshop_operation_receipts_v9; + +CREATE INDEX workshop_operation_receipts_subject_idx + ON workshop_operation_receipts ( + installation_id, + operation, + subject_id, + created_at + ); + +UPDATE workshop_schema +SET version = 10, + package_version = '0.5.0', + updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') +WHERE singleton = 1; diff --git a/migrations/0011_administration_control_plane.sql b/migrations/0011_administration_control_plane.sql new file mode 100644 index 0000000..1d3ebdb --- /dev/null +++ b/migrations/0011_administration_control_plane.sql @@ -0,0 +1,106 @@ +CREATE TABLE workshop_authorization_principals ( + installation_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + lifecycle TEXT NOT NULL CHECK ( + lifecycle IN ('active', 'disabled', 'revoked') + ), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + PRIMARY KEY (installation_id, principal_id) +); + +CREATE TABLE workshop_workspace_memberships ( + installation_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + workspace_id TEXT NOT NULL, + active INTEGER NOT NULL CHECK (active IN (0, 1)), + updated_at TEXT NOT NULL, + PRIMARY KEY (installation_id, principal_id, workspace_id) +); + +CREATE TABLE workshop_oauth_bindings ( + installation_id TEXT NOT NULL, + issuer TEXT NOT NULL, + subject TEXT NOT NULL, + principal_id TEXT NOT NULL, + workspace_id TEXT NOT NULL, + credential_id TEXT NOT NULL, + active INTEGER NOT NULL CHECK (active IN (0, 1)), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + PRIMARY KEY (installation_id, issuer, subject) +); + +CREATE INDEX workshop_oauth_bindings_principal_idx + ON workshop_oauth_bindings ( + installation_id, + principal_id, + workspace_id, + active + ); + +CREATE TABLE workshop_authorization_admin_receipts ( + installation_id TEXT NOT NULL, + actor_principal_id TEXT NOT NULL, + idempotency_key TEXT NOT NULL, + action TEXT NOT NULL, + request_digest TEXT NOT NULL CHECK ( + length(request_digest) = 64 + AND request_digest NOT GLOB '*[^0-9a-f]*' + ), + response_json TEXT NOT NULL CHECK (json_valid(response_json)), + created_at TEXT NOT NULL, + PRIMARY KEY (installation_id, actor_principal_id, idempotency_key) +); + +CREATE TABLE workshop_authorization_audit ( + audit_id TEXT PRIMARY KEY, + installation_id TEXT NOT NULL, + actor_principal_id TEXT NOT NULL, + action TEXT NOT NULL, + target_kind TEXT NOT NULL, + target_id TEXT NOT NULL, + authorization_revision INTEGER NOT NULL CHECK ( + authorization_revision >= 1 + ), + details_json TEXT NOT NULL CHECK (json_valid(details_json)), + created_at TEXT NOT NULL +); + +CREATE INDEX workshop_authorization_audit_installation_idx + ON workshop_authorization_audit ( + installation_id, + created_at DESC, + audit_id + ); + +CREATE TABLE workshop_semantic_configuration_state ( + installation_id TEXT PRIMARY KEY, + revision INTEGER NOT NULL CHECK (revision >= 1), + fingerprint TEXT NOT NULL CHECK ( + length(fingerprint) = 64 + AND fingerprint NOT GLOB '*[^0-9a-f]*' + ), + configuration_json TEXT NOT NULL CHECK (json_valid(configuration_json)), + ready INTEGER NOT NULL CHECK (ready IN (0, 1)), + updated_at TEXT NOT NULL +); + +CREATE TABLE workshop_semantic_configuration_receipts ( + installation_id TEXT NOT NULL, + actor_principal_id TEXT NOT NULL, + idempotency_key TEXT NOT NULL, + request_digest TEXT NOT NULL CHECK ( + length(request_digest) = 64 + AND request_digest NOT GLOB '*[^0-9a-f]*' + ), + response_json TEXT NOT NULL CHECK (json_valid(response_json)), + created_at TEXT NOT NULL, + PRIMARY KEY (installation_id, actor_principal_id, idempotency_key) +); + +UPDATE workshop_schema +SET version = 11, + package_version = '0.5.0', + updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') +WHERE singleton = 1; diff --git a/package-lock.json b/package-lock.json index 959f88c..386d33e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,52 +1,41 @@ { "name": "@gnolith/workshop", - "version": "0.4.2", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gnolith/workshop", - "version": "0.4.2", + "version": "0.5.0", "license": "MIT", "dependencies": { "@traqula/parser-sparql-1-1": "^1.1.8", - "swr": "^2.4.2" + "ajv": "^8.17.1" + }, + "bin": { + "gnolith-workshop": "dist/cli.js" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@gnolith/diamond": "0.4.0", - "@gnolith/taproot": "0.4.0", - "@modelcontextprotocol/sdk": "^1.29.0", - "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.1", + "@gnolith/diamond": "file:../diamond/gnolith-diamond-0.5.0.tgz", + "@gnolith/taproot": "file:../taproot-artifacts/gnolith-taproot-0.5.0.tgz", "@types/node": "^24.0.0", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.3", - "@vitest/coverage-v8": "^3.2.7", - "ajv": "^8.17.1", + "@vitest/coverage-v8": "^4.1.10", "ajv-formats": "^3.0.1", - "axe-core": "^4.11.0", "eslint": "^10.7.0", - "eslint-plugin-react-hooks": "^7.1.1", - "jsdom": "^29.1.1", "miniflare": "^4.20260714.0", "prettier": "^3.9.5", - "react": "^19.2.7", - "react-dom": "^19.2.7", "tar-stream": "^3.1.7", "typescript": "^5.9.2", "typescript-eslint": "^8.64.0", - "vitest": "^3.2.4", - "wrangler": "^4.112.0" + "vitest": "^4.1.10" }, "engines": { "node": ">=22" }, "peerDependencies": { - "@gnolith/diamond": ">=0.4.0 <0.5.0", - "@gnolith/taproot": ">=0.4.0 <0.5.0", - "@gnolith/waystone": ">=0.1.0 <0.3.0", - "react": ">=19 <20" + "@gnolith/diamond": ">=0.5.0-0 <0.6.0", + "@gnolith/taproot": ">=0.5.0-0 <0.6.0" }, "peerDependenciesMeta": { "@gnolith/diamond": { @@ -54,249 +43,9 @@ }, "@gnolith/taproot": { "optional": true - }, - "@gnolith/waystone": { - "optional": true - }, - "react": { - "optional": true } } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@csstools/css-calc": "^3.2.0", - "@csstools/css-color-parser": "^4.1.0", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", - "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/generational-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/code-frame/node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -317,30 +66,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", @@ -357,50 +82,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/types": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", @@ -438,45 +119,6 @@ "buffer": "^6.0.3" } }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, - "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", - "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", - "dev": true, - "license": "MIT OR Apache-2.0", - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/@cloudflare/unenv-preset": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", - "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", - "dev": true, - "license": "MIT OR Apache-2.0", - "peerDependencies": { - "unenv": "2.0.0-rc.24", - "workerd": ">1.20260305.0 <2.0.0-0" - }, - "peerDependenciesMeta": { - "workerd": { - "optional": true - } - } - }, "node_modules/@cloudflare/workerd-darwin-64": { "version": "1.20260714.1", "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260714.1.tgz", @@ -5971,609 +5613,50 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@csstools/color-helpers": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", - "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=20.19.0" + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" } }, - "node_modules/@csstools/css-calc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", - "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" } }, - "node_modules/@csstools/css-color-parser": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", - "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], "license": "MIT", + "optional": true, "dependencies": { - "@csstools/color-helpers": "^6.1.0", - "@csstools/css-calc": "^3.2.1" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "tslib": "^2.4.0" } }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", - "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", - "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@so-ric/colorspace": "^1.1.6", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", - "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { @@ -6704,28 +5787,10 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, "node_modules/@gnolith/diamond": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@gnolith/diamond/-/diamond-0.4.0.tgz", - "integrity": "sha512-Njk/d5YESyqHiPQinN7jl+kHnlVCF3TkpmTnGjaGfdfaXc3CwELNWouu562aBwCrhKMz97yKU0y8NO48OujYdQ==", + "version": "0.5.0", + "resolved": "file:../diamond/gnolith-diamond-0.5.0.tgz", + "integrity": "sha512-KrgOpBB640UoNjFMiDqN8DgB4VYBDbiKRtZ4bKKR4kETN3QzPU9hUBEyNXZsyLA2IbE4YlQka/GflrsP7aNU8g==", "dev": true, "license": "MIT", "dependencies": { @@ -6740,13 +5805,13 @@ } }, "node_modules/@gnolith/taproot": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@gnolith/taproot/-/taproot-0.4.0.tgz", - "integrity": "sha512-yYxbrUNnu74zBaxHoywGlgeG2LFz4HMzi2RLcsq83/JVEIkwbWvWZ8tuLpxFYxTAgTXG1/FHddgEJStRupe54A==", + "version": "0.5.0", + "resolved": "file:../taproot-artifacts/gnolith-taproot-0.5.0.tgz", + "integrity": "sha512-uVA0FBUyQA3SwNGtxk93mHeoZibzzddHvbmSJ8krZKWekLbHSGNDtYBCmtPTpMZPhlrWMJYYBO+7NjpFy1Tkbg==", "dev": true, "license": "MIT", "dependencies": { - "@gnolith/diamond": "0.4.0", + "@gnolith/diamond": "0.5.0", "@rdfjs/types": "^2.0.1", "rdf-data-factory": "^2.0.2" }, @@ -6754,19 +5819,6 @@ "node": ">=22" } }, - "node_modules/@hono/node-server": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.11.tgz", - "integrity": "sha512-bjD221KPLoJTWUwso1J6fGKiTXEUFedG/s0visavY4zakFPkeGURMRNly+FhBHs7T8Dz4qHaZIMX9ZoJHSJtKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "hono": "^4" - } - }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -7360,34 +6412,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jeswr/prefixcc": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jeswr/prefixcc/-/prefixcc-1.2.1.tgz", @@ -7411,28 +6435,6 @@ "event-emitter-promisify": "^1.1.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -7461,56 +6463,33 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" + "@tybys/wasm-util": "^0.10.3" }, - "engines": { - "node": ">=18" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@poppinss/colors": { @@ -7565,24 +6544,10 @@ "@types/node": "*" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -7591,12 +6556,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -7605,12 +6573,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -7619,26 +6590,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -7646,181 +6606,101 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ - "ppc64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ - "s390x" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ - "x64" + "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ "x64" ], @@ -7829,12 +6709,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ "x64" ], @@ -7842,13 +6725,16 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ "arm64" ], @@ -7857,54 +6743,62 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", - "cpu": [ - "ia32" - ], + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "tslib": "^2.4.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -7913,7 +6807,17 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" }, "node_modules/@rubensworks/saxes": { "version": "6.0.1", @@ -7959,68 +6863,12 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } + "license": "MIT" }, "node_modules/@traqula/algebra-sparql-1-1": { "version": "1.1.8", @@ -8182,13 +7030,16 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", - "peer": true + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } }, "node_modules/@types/chai": { "version": "5.2.3", @@ -8281,26 +7132,6 @@ "undici-types": "~7.18.0" } }, - "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, "node_modules/@types/readable-stream": { "version": "4.0.24", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz", @@ -8604,32 +7435,29 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.7.tgz", - "integrity": "sha512-NEGWJS2XNu2PfRLQwOO3CTKj1tTETxNBdk454vDxVBhxJYhPaA/eS0nAI0c+1El1P7a60z8+i+ZrQoGESweGKg==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", - "ast-v8-to-istanbul": "^0.3.3", - "debug": "^4.4.1", + "@vitest/utils": "4.1.10", + "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.2.7", - "vitest": "3.2.7" + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -8638,39 +7466,40 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.7", + "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -8682,42 +7511,42 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.7", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.7", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { @@ -8725,28 +7554,25 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", - "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", - "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.7", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -8765,20 +7591,6 @@ "node": ">=6.5" } }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -8806,7 +7618,6 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -8837,43 +7648,6 @@ } } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/arrayify-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrayify-stream/-/arrayify-stream-2.0.1.tgz", @@ -8892,9 +7666,9 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", - "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", + "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", "dev": true, "license": "MIT", "dependencies": { @@ -8903,13 +7677,6 @@ "js-tokens": "^10.0.0" } }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -8937,16 +7704,6 @@ "asynciterator": "^3.9.0" } }, - "node_modules/axe-core": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", - "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, "node_modules/b4a": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", @@ -9057,148 +7814,45 @@ "bare-path": "^3.0.0" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.44", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.44.tgz", - "integrity": "sha512-T3ghW+sl/ZJ8w1v/yQx3qvJ9040DWoLBz8JT/CILbAKcFyG9b2MRe75v6W5uXjv6uH1lumK2Kv46y2zSkcej0Q==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, - "node_modules/bignumber.js": { - "version": "11.1.5", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.5.tgz", - "integrity": "sha512-6WmzCNtUnfKpbozq+hOgWaZMMzORmYBwF1xZScyoIX3QRYWeKTtxxwDOW5tIz7C9BdjkIYHGTcelCLkXg0mndw==", - "dev": true, - "license": "MIT" - }, - "node_modules/blake3-wasm": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", - "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", - "dev": true, - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/browserslist": { - "version": "4.28.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", - "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "funding": [ { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" + "type": "github", + "url": "https://github.com/sponsors/feross" }, { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" + "type": "patreon", + "url": "https://www.patreon.com/feross" }, { - "type": "github", - "url": "https://github.com/sponsors/ai" + "type": "consulting", + "url": "https://feross.org/support" } ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.5.tgz", + "integrity": "sha512-6WmzCNtUnfKpbozq+hOgWaZMMzORmYBwF1xZScyoIX3QRYWeKTtxxwDOW5tIz7C9BdjkIYHGTcelCLkXg0mndw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "dev": true, "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.42", - "caniuse-lite": "^1.0.30001803", - "electron-to-chromium": "^1.5.389", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" + "balanced-match": "^4.0.2" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": "20 || >=22" } }, "node_modules/buffer": { @@ -9226,78 +7880,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001806", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", - "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, "node_modules/canonicalize": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-1.0.8.tgz", @@ -9306,32 +7888,15 @@ "license": "Apache-2.0" }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -9569,30 +8134,6 @@ "dev": true, "license": "MIT" }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -9614,34 +8155,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/cross-fetch": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", @@ -9667,41 +8180,6 @@ "node": ">= 8" } }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -9720,23 +8198,6 @@ } } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -9744,25 +8205,6 @@ "dev": true, "license": "MIT" }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -9773,14 +8215,6 @@ "node": ">=8" } }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -9853,49 +8287,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.393", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.393.tgz", - "integrity": "sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", @@ -9903,29 +8294,6 @@ "dev": true, "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/error-stack-parser-es": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", @@ -9936,88 +8304,13 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "dev": true, "license": "MIT" }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -10028,13 +8321,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -10095,36 +8381,16 @@ "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" + "funding": { + "url": "https://eslint.org/donate" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-scope": { @@ -10257,16 +8523,6 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/event-emitter-promisify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/event-emitter-promisify/-/event-emitter-promisify-1.1.0.tgz", @@ -10304,29 +8560,6 @@ "bare-events": "^2.7.0" } }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", - "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -10337,85 +8570,10 @@ "node": ">=12.0.0" } }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz", - "integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-fifo": { @@ -10443,7 +8601,6 @@ "version": "3.1.4", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", - "dev": true, "funding": [ { "type": "github", @@ -10522,28 +8679,6 @@ "node": ">=16.0.0" } }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -10589,43 +8724,6 @@ "dev": true, "license": "MIT" }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -10641,26 +8739,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -10671,67 +8749,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -10745,52 +8762,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graphql": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/graphql/-/graphql-17.0.2.tgz", @@ -10871,19 +8842,6 @@ "node": ">=8" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -10895,59 +8853,6 @@ "minimalistic-assert": "^1.0.1" } }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/hono": { - "version": "4.12.31", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz", - "integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -10995,27 +8900,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/http-link-header": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.4.tgz", @@ -11026,23 +8910,6 @@ "node": ">=6.0.0" } }, - "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -11098,26 +8965,6 @@ "dev": true, "license": "ISC" }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -11151,20 +8998,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -11203,23 +9036,8 @@ "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { "node": ">=10" @@ -11239,103 +9057,13 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jose": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", - "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, - "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", - "css-tree": "^3.2.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", - "parse5": "^8.0.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -11347,16 +9075,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, "license": "MIT" }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -11364,19 +9084,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/jsonld-context-parser": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/jsonld-context-parser/-/jsonld-context-parser-2.4.0.tgz", @@ -11566,6 +9273,267 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "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" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -11600,13 +9568,6 @@ "node": ">= 12.0.0" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -11614,17 +9575,6 @@ "dev": true, "license": "ISC" }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -11636,15 +9586,15 @@ } }, "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" } }, "node_modules/make-dir": { @@ -11663,46 +9613,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/microdata-rdf-streaming-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/microdata-rdf-streaming-parser/-/microdata-rdf-streaming-parser-3.0.0.tgz", @@ -11753,33 +9663,6 @@ "entities": "^4.5.0" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/miniflare": { "version": "4.20260714.0", "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260714.0.tgz", @@ -11834,16 +9717,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -11897,16 +9770,6 @@ "integrity": "sha512-KBCIM4dAIT9j/pSXLHHQbZG74NmKNXTtxU2zHN0HG6uzzuFE01m1UdGoUmVHmACiBuCAOL7KwfqSW1oUQBj/vg==", "dev": true }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -11953,26 +9816,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/node-releases": { - "version": "2.0.51", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", - "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -11986,27 +9829,18 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" + "node": ">=12.20.0" } }, "node_modules/one-time": { @@ -12069,36 +9903,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -12119,30 +9923,6 @@ "node": ">=8" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "dev": true, - "license": "MIT" - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -12150,16 +9930,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -12180,20 +9950,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, "node_modules/postcss": { - "version": "8.5.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", - "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "dev": true, "funding": [ { @@ -12245,47 +10005,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -12303,20 +10022,6 @@ "dev": true, "license": "MIT" }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -12327,53 +10032,6 @@ "node": ">=6" } }, - "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", - "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/rdf-data-factory": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/rdf-data-factory/-/rdf-data-factory-2.0.2.tgz", @@ -13318,36 +10976,6 @@ "url": "https://github.com/sponsors/rubensworks/" } }, - "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.7" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/readable-from-web": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/readable-from-web/-/readable-from-web-1.0.0.tgz", @@ -13408,83 +11036,43 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.9" + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, "node_modules/safe-buffer": { @@ -13518,33 +11106,6 @@ "node": ">=10" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -13555,62 +11116,8 @@ "semver": "bin/semver.js" }, "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" + "node": ">=10" + } }, "node_modules/shaclc-parse": { "version": "2.0.0", @@ -13708,82 +11215,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -13791,19 +11222,6 @@ "dev": true, "license": "ISC" }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -14005,20 +11423,10 @@ "dev": true, "license": "MIT" }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -14061,123 +11469,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -14191,26 +11482,6 @@ "node": ">=8" } }, - "node_modules/swr": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.2.tgz", - "integrity": "sha512-ej644Y2bvkIajfR32KGeSSdBXQW+ScjGjkybZgSE7kFpk9eGnV44XY9FJylXi+W75pavSX1PVNB57W5EbhGIYw==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, "node_modules/tar-stream": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", @@ -14234,21 +11505,6 @@ "streamx": "^2.12.5" } }, - "node_modules/test-exclude": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", - "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^10.2.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/text-decoder": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", @@ -14281,11 +11537,14 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { "version": "0.2.17", @@ -14304,92 +11563,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" } }, - "node_modules/tldts": { - "version": "7.4.9", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", - "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.4.9" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.4.9", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", - "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tough-cookie": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", - "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -14427,44 +11610,11 @@ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "prelude-ls": "^1.2.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "engines": { + "node": ">= 0.8.0" } }, "node_modules/typescript": { @@ -14522,57 +11672,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unenv": { - "version": "2.0.0-rc.24", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", - "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pathe": "^2.0.3" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -14589,15 +11688,6 @@ "integrity": "sha512-enADBvHfhjrwxFMTVWeIIYz51SZ91uC6o2MR/NQTVljJB6HTZ8eQL3Q7JBj3RxNISA14MOwJaU3vpf5R6dyxHA==", "dev": true }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -14626,29 +11716,18 @@ "dev": true, "license": "MIT" }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/vite": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", - "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0 || ^0.28.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -14664,9 +11743,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -14679,13 +11759,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -14711,89 +11794,80 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vitest": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", - "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.7", - "@vitest/mocker": "3.2.7", - "@vitest/pretty-format": "^3.2.7", - "@vitest/runner": "3.2.7", - "@vitest/snapshot": "3.2.7", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.7", - "@vitest/ui": "3.2.7", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@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": { @@ -14804,57 +11878,12 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -14987,147 +12016,6 @@ "@cloudflare/workerd-windows-64": "1.20260714.1" } }, - "node_modules/wrangler": { - "version": "4.112.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.112.0.tgz", - "integrity": "sha512-5H+XUD0TySCv1LuktFHDIEOkboH2nTfQs+35L+USt3MtntjDTMVIJprLgQcL2WBjulOyjxpd1vyTiSTJVW5MjQ==", - "dev": true, - "license": "MIT OR Apache-2.0", - "dependencies": { - "@cloudflare/kv-asset-handler": "0.5.0", - "@cloudflare/unenv-preset": "2.16.1", - "blake3-wasm": "2.1.5", - "esbuild": "0.28.1", - "miniflare": "4.20260714.0", - "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.24", - "workerd": "1.20260714.1" - }, - "bin": { - "cf-wrangler": "bin/cf-wrangler.js", - "wrangler": "bin/wrangler.js", - "wrangler2": "bin/wrangler.js" - }, - "engines": { - "node": ">=22.0.0" - }, - "optionalDependencies": { - "fsevents": "2.3.3" - }, - "peerDependencies": { - "@cloudflare/workers-types": "^5.20260714.1" - }, - "peerDependenciesMeta": { - "@cloudflare/workers-types": { - "optional": true - } - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, "node_modules/ws": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", @@ -15150,16 +12038,6 @@ } } }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -15177,13 +12055,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, "node_modules/yargs": { "version": "17.7.3", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", @@ -15295,39 +12166,6 @@ "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } } } } diff --git a/package.json b/package.json index cf90db8..23d2a06 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@gnolith/workshop", - "version": "0.4.2", - "description": "Hosted agent tasks, memories, coordination, and tooling.", + "version": "0.5.0", + "description": "Self-hosted Gnolith MCP and BFF executable.", "type": "module", "private": false, "sideEffects": false, @@ -16,7 +16,7 @@ "author": "Gnolith contributors", "keywords": [ "agents", - "cloudflare-d1", + "sqlite", "mcp", "research", "sparql" @@ -25,19 +25,22 @@ "access": "public", "provenance": true }, + "bin": { + "gnolith-workshop": "./dist/cli.js" + }, "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }, - "./core": { - "types": "./dist/core.d.ts", - "import": "./dist/core.js" - }, "./protocol": { "types": "./dist/protocol.d.ts", "import": "./dist/protocol.js" }, + "./client": { + "types": "./dist/client.d.ts", + "import": "./dist/client.js" + }, "./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" @@ -46,23 +49,32 @@ "types": "./dist/mcp.d.ts", "import": "./dist/mcp.js" }, - "./site": { - "types": "./dist/site.d.ts", - "import": "./dist/site.js" + "./executable": { + "types": "./dist/executable.d.ts", + "import": "./dist/executable.js" + }, + "./local-adapter": { + "types": "./dist/local-adapter.d.ts", + "import": "./dist/local-adapter.js" + }, + "./bootstrap": { + "types": "./dist/bootstrap.d.ts", + "import": "./dist/bootstrap.js" }, - "./ui": { - "types": "./dist/ui.d.ts", - "import": "./dist/ui.js" + "./backup": { + "types": "./dist/backup.d.ts", + "import": "./dist/backup.js" }, "./migrations": { "types": "./dist/migrations.d.ts", "import": "./dist/migrations.js" }, - "./styles.css": "./dist/styles.css", + "./schemas/*": "./schemas/*", "./package.json": "./package.json" }, "files": [ "dist", + "schemas", "docs", "migrations", "CHANGELOG.md", @@ -72,18 +84,16 @@ "SECURITY.md" ], "scripts": { - "build": "node scripts/clean.mjs && tsc -p tsconfig.build.json && node scripts/copy-assets.mjs", - "check": "npm run format:check && npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm run performance:check && npm run parity:node-sqlite && npm run security:dev-check && npm run worker:check && npm run worker:test && npm run pack:check && npm run release:boundary-check && npm run consumer:check && npm run artifact:verify && npm run audit:check && npm run readiness:check", + "build": "node scripts/clean.mjs && node scripts/generate-operation-contract.mjs --check && tsc -p tsconfig.build.json", + "check": "npm run format:check && npm run lint && npm run typecheck && npm test && npm run build && npm run pack:check && npm run audit:check", "artifact:path": "node scripts/artifact-path.mjs", "artifact:prepare": "node scripts/prepare-artifact.mjs", "artifact:verify": "node scripts/verify-artifact.mjs", "audit:check": "npm audit --omit=dev --audit-level=high", - "consumer:check": "node scripts/consumer-smoke.mjs && npm run consumer:waystone-check", - "consumer:waystone-check": "node scripts/waystone-consumer-smoke.mjs", "format": "prettier --write .", "format:check": "prettier --check .", "lint": "eslint src test scripts", - "pack:check": "npm run artifact:prepare", + "pack:check": "npm pack --dry-run --json", "performance:check": "node scripts/performance-check.mjs", "parity:node-sqlite": "node scripts/node-sqlite-parity.mjs", "prepack": "npm run build", @@ -95,8 +105,8 @@ "test": "vitest run", "test:coverage": "vitest run --coverage", "typecheck": "tsc --noEmit", - "worker:check": "wrangler deploy --dry-run --config examples/package-runtime-canary/wrangler.jsonc --outdir ../../.wrangler/package-runtime-canary", - "worker:test": "node scripts/worker-e2e.mjs" + "candidate:prepare": "node scripts/prepare-candidate.mjs", + "packed:process-check": "node scripts/packed-process-smoke.mjs" }, "engines": { "node": ">=22" @@ -104,13 +114,11 @@ "packageManager": "npm@11.9.0", "dependencies": { "@traqula/parser-sparql-1-1": "^1.1.8", - "swr": "^2.4.2" + "ajv": "^8.17.1" }, "peerDependencies": { - "@gnolith/diamond": ">=0.4.0 <0.5.0", - "@gnolith/taproot": ">=0.4.0 <0.5.0", - "@gnolith/waystone": ">=0.1.0 <0.3.0", - "react": ">=19 <20" + "@gnolith/diamond": ">=0.5.0-0 <0.6.0", + "@gnolith/taproot": ">=0.5.0-0 <0.6.0" }, "peerDependenciesMeta": { "@gnolith/diamond": { @@ -118,50 +126,33 @@ }, "@gnolith/taproot": { "optional": true - }, - "@gnolith/waystone": { - "optional": true - }, - "react": { - "optional": true } }, "devDependencies": { "@eslint/js": "^10.0.1", - "@gnolith/diamond": "0.4.0", - "@gnolith/taproot": "0.4.0", - "@modelcontextprotocol/sdk": "^1.29.0", - "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.1", + "@gnolith/diamond": "file:../diamond/gnolith-diamond-0.5.0.tgz", + "@gnolith/taproot": "file:../taproot-artifacts/gnolith-taproot-0.5.0.tgz", "@types/node": "^24.0.0", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.3", - "@vitest/coverage-v8": "^3.2.7", - "ajv": "^8.17.1", + "@vitest/coverage-v8": "^4.1.10", "ajv-formats": "^3.0.1", - "axe-core": "^4.11.0", "eslint": "^10.7.0", - "eslint-plugin-react-hooks": "^7.1.1", - "jsdom": "^29.1.1", "miniflare": "^4.20260714.0", "prettier": "^3.9.5", - "react": "^19.2.7", - "react-dom": "^19.2.7", "tar-stream": "^3.1.7", "typescript": "^5.9.2", "typescript-eslint": "^8.64.0", - "vitest": "^3.2.4", - "wrangler": "^4.112.0" + "vitest": "^4.1.10" }, "overrides": { "@hono/node-server": "2.0.11", + "brace-expansion": "5.0.8", "sharp": "0.35.3" }, "license": "MIT", - "main": "eslint.config.js", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "directories": { "doc": "docs", - "example": "examples", "test": "test" } } diff --git a/schemas/fixtures/workshop-status-evidence-v1.json b/schemas/fixtures/workshop-status-evidence-v1.json new file mode 100644 index 0000000..515cf3f --- /dev/null +++ b/schemas/fixtures/workshop-status-evidence-v1.json @@ -0,0 +1,198 @@ +{ + "format": "gnolith-workshop-status-evidence-fixtures-v1", + "version": 1, + "operationSchemaVersion": 2, + "fixtures": { + "ready": { + "installationId": "installation:status-fixture", + "baseIri": "https://status-fixture.invalid/", + "principalId": "principal:status-fixture", + "credentialId": "credential:status-fixture", + "activeWorkspaceId": "workspace:status-fixture", + "capabilities": ["read"], + "authorizationRevision": 7, + "migrationReadiness": { + "namespace": "@gnolith/workshop", + "version": 11, + "ready": true + }, + "compatibility": { "diamond": true, "taproot": true }, + "canonicalReady": true, + "authorizationReady": true, + "lexicalReady": true, + "producers": { + "ready": true, + "fingerprint": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "kinds": ["task", "memory", "prompt"] + }, + "blobReady": true, + "versions": { "server": "0.5.0", "operationSchema": 2 }, + "operationCatalogDigest": "af455b4d6ab3560bca9c0ab48e9db7bd3cbcfa166ace875578d88d182069071f", + "semanticState": { + "state": "ready", + "configured": true, + "revision": 1, + "fingerprint": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "ready": true, + "diagnostic": null + } + }, + "materializationPendingSqlite": { + "installationId": "installation:status-fixture", + "baseIri": "https://status-fixture.invalid/", + "principalId": "principal:status-fixture", + "credentialId": "credential:status-fixture", + "activeWorkspaceId": "workspace:status-fixture", + "capabilities": ["read"], + "authorizationRevision": 7, + "migrationReadiness": { + "namespace": "@gnolith/workshop", + "version": 11, + "ready": true + }, + "compatibility": { "diamond": true, "taproot": true }, + "canonicalReady": true, + "authorizationReady": true, + "lexicalReady": true, + "producers": { + "ready": true, + "fingerprint": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "kinds": ["task", "memory", "prompt"] + }, + "blobReady": true, + "versions": { "server": "0.5.0", "operationSchema": 2 }, + "operationCatalogDigest": "af455b4d6ab3560bca9c0ab48e9db7bd3cbcfa166ace875578d88d182069071f", + "semanticState": { + "state": "degraded", + "configured": true, + "revision": 1, + "fingerprint": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "ready": false, + "diagnostic": { + "format": "gnolith-workshop-semantic-diagnostic-v1", + "version": 1, + "code": "materialization-pending", + "retryable": true, + "backend": "sqlite", + "repair": "retry" + } + } + }, + "providerUnavailableQdrant": { + "installationId": "installation:status-fixture", + "baseIri": "https://status-fixture.invalid/", + "principalId": "principal:status-fixture", + "credentialId": "credential:status-fixture", + "activeWorkspaceId": "workspace:status-fixture", + "capabilities": ["read"], + "authorizationRevision": 7, + "migrationReadiness": { + "namespace": "@gnolith/workshop", + "version": 11, + "ready": true + }, + "compatibility": { "diamond": true, "taproot": true }, + "canonicalReady": true, + "authorizationReady": true, + "lexicalReady": true, + "producers": { + "ready": true, + "fingerprint": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "kinds": ["task", "memory", "prompt"] + }, + "blobReady": true, + "versions": { "server": "0.5.0", "operationSchema": 2 }, + "operationCatalogDigest": "af455b4d6ab3560bca9c0ab48e9db7bd3cbcfa166ace875578d88d182069071f", + "semanticState": { + "state": "degraded", + "configured": true, + "revision": 2, + "fingerprint": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "ready": false, + "diagnostic": { + "format": "gnolith-workshop-semantic-diagnostic-v1", + "version": 1, + "code": "provider-unavailable", + "retryable": true, + "backend": "qdrant", + "repair": "retry" + } + } + }, + "providerUnavailableAfterRestart": { + "installationId": "installation:status-fixture", + "baseIri": "https://status-fixture.invalid/", + "principalId": "principal:status-fixture", + "credentialId": "credential:status-fixture", + "activeWorkspaceId": "workspace:status-fixture", + "capabilities": ["read"], + "authorizationRevision": 7, + "migrationReadiness": { + "namespace": "@gnolith/workshop", + "version": 11, + "ready": true + }, + "compatibility": { "diamond": true, "taproot": true }, + "canonicalReady": true, + "authorizationReady": true, + "lexicalReady": true, + "producers": { + "ready": true, + "fingerprint": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "kinds": ["task", "memory", "prompt"] + }, + "blobReady": true, + "versions": { "server": "0.5.0", "operationSchema": 2 }, + "operationCatalogDigest": "af455b4d6ab3560bca9c0ab48e9db7bd3cbcfa166ace875578d88d182069071f", + "semanticState": { + "state": "degraded", + "configured": true, + "revision": 1, + "fingerprint": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "ready": false, + "diagnostic": { + "format": "gnolith-workshop-semantic-diagnostic-v1", + "version": 1, + "code": "provider-unavailable", + "retryable": true, + "backend": "sqlite", + "repair": "retry" + } + } + }, + "unconfigured": { + "installationId": "installation:status-fixture", + "baseIri": "https://status-fixture.invalid/", + "principalId": "principal:status-fixture", + "credentialId": "credential:status-fixture", + "activeWorkspaceId": "workspace:status-fixture", + "capabilities": ["read"], + "authorizationRevision": 7, + "migrationReadiness": { + "namespace": "@gnolith/workshop", + "version": 11, + "ready": true + }, + "compatibility": { "diamond": true, "taproot": true }, + "canonicalReady": true, + "authorizationReady": true, + "lexicalReady": true, + "producers": { + "ready": true, + "fingerprint": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "kinds": ["task", "memory", "prompt"] + }, + "blobReady": true, + "versions": { "server": "0.5.0", "operationSchema": 2 }, + "operationCatalogDigest": "af455b4d6ab3560bca9c0ab48e9db7bd3cbcfa166ace875578d88d182069071f", + "semanticState": { + "state": "unconfigured", + "configured": false, + "revision": null, + "fingerprint": null, + "ready": false, + "diagnostic": null + } + } + } +} diff --git a/schemas/gnolith-operations-v1.json b/schemas/gnolith-operations-v1.json new file mode 100644 index 0000000..5a89787 --- /dev/null +++ b/schemas/gnolith-operations-v1.json @@ -0,0 +1,6809 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://gnolith.dev/schemas/workshop/operations-v1.json", + "title": "Gnolith Workshop operation catalog", + "format": "gnolith-operation-catalog-v1", + "operationSchemaVersion": 2, + "routePrefix": "/api/v1/operations", + "operations": [ + { + "name": "gnolith_status", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/gnolith_status", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": { + "installationId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "baseIri": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "credentialId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "activeWorkspaceId": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + { + "type": "null" + } + ] + }, + "capabilities": { + "type": "array", + "maxItems": 256, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "authorizationRevision": { + "type": "integer", + "minimum": 0, + "maximum": 1000000000 + }, + "migrationReadiness": { + "type": "object", + "properties": { + "namespace": { + "const": "@gnolith/workshop" + }, + "version": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "ready": { + "type": "boolean" + } + }, + "required": [ + "namespace", + "version", + "ready" + ], + "additionalProperties": false + }, + "compatibility": { + "type": "object", + "properties": { + "diamond": { + "type": "boolean" + }, + "taproot": { + "type": "boolean" + } + }, + "required": [ + "diamond", + "taproot" + ], + "additionalProperties": false + }, + "canonicalReady": { + "type": "boolean" + }, + "authorizationReady": { + "type": "boolean" + }, + "lexicalReady": { + "type": "boolean" + }, + "semanticState": { + "type": "object", + "properties": { + "state": { + "enum": [ + "ready", + "degraded", + "unconfigured" + ] + }, + "configured": { + "type": "boolean" + }, + "revision": { + "anyOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + { + "type": "null" + } + ] + }, + "fingerprint": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + { + "type": "null" + } + ] + }, + "ready": { + "type": "boolean" + }, + "diagnostic": { + "anyOf": [ + { + "type": "object", + "properties": { + "format": { + "const": "gnolith-workshop-semantic-diagnostic-v1" + }, + "version": { + "const": 1 + }, + "code": { + "enum": [ + "materialization-pending", + "provider-unavailable" + ] + }, + "retryable": { + "type": "boolean" + }, + "backend": { + "enum": [ + "sqlite", + "qdrant" + ] + }, + "repair": { + "const": "retry" + } + }, + "required": [ + "format", + "version", + "code", + "retryable", + "backend", + "repair" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "state", + "configured", + "revision", + "fingerprint", + "ready", + "diagnostic" + ], + "additionalProperties": false + }, + "producers": { + "type": "object", + "properties": { + "ready": { + "type": "boolean" + }, + "fingerprint": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "kinds": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "uniqueItems": true, + "items": { + "enum": [ + "task", + "memory", + "prompt" + ] + } + } + }, + "required": [ + "ready", + "fingerprint", + "kinds" + ], + "additionalProperties": false + }, + "blobReady": { + "type": "boolean" + }, + "versions": { + "type": "object", + "properties": { + "server": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "operationSchema": { + "const": 2 + } + }, + "required": [ + "server", + "operationSchema" + ], + "additionalProperties": false + }, + "operationCatalogDigest": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "installationId", + "baseIri", + "principalId", + "credentialId", + "activeWorkspaceId", + "capabilities", + "authorizationRevision", + "migrationReadiness", + "compatibility", + "canonicalReady", + "authorizationReady", + "lexicalReady", + "semanticState", + "producers", + "blobReady", + "versions", + "operationCatalogDigest" + ], + "additionalProperties": false + } + }, + { + "name": "authorization_admin", + "version": 1, + "capability": "admin", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/authorization_admin", + "inputSchema": { + "oneOf": [ + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "principal-create" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principal": { + "type": "object", + "properties": { + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "principalId" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principal" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "principal-enable" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principalId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "principal-disable" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principalId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "principal-revoke" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principalId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "workspace-membership-add" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principalId", + "workspaceId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "workspace-membership-remove" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principalId", + "workspaceId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "workspace-grant-add" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "capability": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principalId", + "workspaceId", + "capability" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "workspace-grant-remove" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "capability": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principalId", + "workspaceId", + "capability" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "credential-register" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "credential": { + "type": "object", + "properties": { + "credentialId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "tokenDigest": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "Lowercase hexadecimal SHA-256 of UTF-8 \"gnolith-local-bearer-v1\\u0000\" followed by the 32 bytes obtained by base64url-decoding the exact 43-character bearer token." + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "credentialId", + "tokenDigest", + "principalId", + "workspaceId" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "credential" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "credential-revoke" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "credentialId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "credentialId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "oauth-bind" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "binding": { + "type": "object", + "properties": { + "issuer": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "subject": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "credentialId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "issuer", + "subject", + "principalId", + "workspaceId" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "binding" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "oauth-unbind" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "issuer": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "subject": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "issuer", + "subject" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "visibility-policy-set" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "target": { + "type": "object", + "properties": { + "kind": { + "enum": [ + "entity", + "resource", + "annotation" + ] + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "kind", + "id" + ], + "additionalProperties": false + }, + "expectedTargetRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "visibility": { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "clauses": { + "description": "Canonical CNF visibility: clauses are ANDed; atoms within each clause are ORed. The resulting scope must retain the administering principal.", + "type": "array", + "maxItems": 32, + "items": { + "type": "array", + "minItems": 1, + "maxItems": 32, + "items": { + "description": "One alternative inside a visibility clause. Atoms in the same clause are ORed.", + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "const": "public" + } + }, + "required": [ + "kind" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "principal" + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "kind", + "principalId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "workspace" + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "kind", + "workspaceId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "capability" + }, + "capability": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "kind", + "capability" + ], + "additionalProperties": false + } + ] + } + } + } + }, + "required": [ + "version", + "clauses" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "target", + "expectedTargetRevision", + "visibility" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "status" + }, + "target": { + "type": "object", + "properties": { + "kind": { + "enum": [ + "entity", + "resource", + "annotation" + ] + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "kind", + "id" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "action" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "effective-policy" + }, + "target": { + "type": "object", + "properties": { + "kind": { + "enum": [ + "entity", + "resource", + "annotation" + ] + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "kind", + "id" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "action", + "target" + ], + "additionalProperties": false + } + ] + }, + "outputSchema": { + "oneOf": [ + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "status": { + "enum": [ + "committed", + "replayed" + ] + }, + "policyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "auditId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "target": { + "type": "object", + "properties": { + "kind": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "kind", + "id" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "action", + "status", + "policyRevision", + "auditId", + "target" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "enum": [ + "status", + "effective-policy" + ] + }, + "policyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "value": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "version", + "action", + "policyRevision", + "value" + ], + "additionalProperties": false + } + ] + } + }, + { + "name": "search", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/search", + "inputSchema": { + "type": "object", + "properties": { + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "kinds": { + "type": "array", + "maxItems": 7, + "uniqueItems": true, + "items": { + "enum": [ + "statement", + "item", + "task", + "memory", + "prompt", + "resource", + "annotation" + ] + } + }, + "cursor": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "text" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "search_hydrate", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/search_hydrate", + "inputSchema": { + "type": "object", + "properties": { + "kind": { + "enum": [ + "statement", + "item", + "task", + "memory", + "prompt", + "resource", + "annotation" + ] + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "entityType": { + "enum": [ + "item", + "property" + ] + } + }, + "required": [ + "kind", + "id", + "revision" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "search_admin", + "version": 1, + "capability": "search:admin", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": false + }, + "route": "/api/v1/operations/search_admin", + "inputSchema": { + "oneOf": [ + { + "type": "object", + "properties": { + "action": { + "const": "configure" + }, + "options": { + "type": "object", + "properties": { + "expectedRevision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "configuration": { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "provider": { + "type": "object", + "properties": { + "kind": { + "enum": [ + "openai-compatible", + "ollama-compatible" + ] + }, + "endpoint": { + "type": "string", + "pattern": "^https?://", + "maxLength": 4096, + "description": "Exact embeddings POST URL: openai-compatible ends in /embeddings and ollama-compatible ends in /api/embed." + }, + "model": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "dimensions": { + "type": "integer", + "minimum": 1, + "maximum": 65536 + }, + "metric": { + "enum": [ + "cosine", + "dot", + "euclid" + ] + }, + "credentialSelector": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + { + "type": "null" + } + ] + }, + "allowPrivateEndpoint": { + "type": "boolean" + }, + "redirectPolicy": { + "const": "error" + } + }, + "required": [ + "kind", + "endpoint", + "model", + "dimensions", + "metric", + "credentialSelector", + "allowPrivateEndpoint", + "redirectPolicy" + ], + "additionalProperties": false + }, + "vector": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "const": "sqlite" + } + }, + "required": [ + "kind" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "qdrant" + }, + "endpoint": { + "type": "string", + "pattern": "^https?://", + "maxLength": 4096 + }, + "collection": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "credentialSelector": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + { + "type": "null" + } + ] + }, + "allowPrivateEndpoint": { + "type": "boolean" + }, + "redirectPolicy": { + "const": "error" + } + }, + "required": [ + "kind", + "endpoint", + "collection", + "credentialSelector", + "allowPrivateEndpoint", + "redirectPolicy" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "version", + "id", + "name", + "provider", + "vector" + ], + "additionalProperties": false + } + }, + "required": [ + "expectedRevision", + "idempotencyKey", + "configuration" + ], + "additionalProperties": false + } + }, + "required": [ + "action", + "options" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "action": { + "enum": [ + "health", + "materialize", + "retry-dead", + "rebuild", + "activate", + "semantic-select", + "semantic-reconnect", + "semantic-estimate", + "semantic-approve", + "semantic-run", + "semantic-resume", + "semantic-pause", + "semantic-stop", + "semantic-retry", + "semantic-exclude", + "semantic-retire", + "semantic-delete" + ] + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "cursor": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "options": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "action" + ], + "additionalProperties": false + } + ] + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_entity", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_entity", + "inputSchema": { + "type": "object", + "properties": { + "entityId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "entityId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "entity_history", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/entity_history", + "inputSchema": { + "type": "object", + "properties": { + "entityId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "entityId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "statement_history", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/statement_history", + "inputSchema": { + "type": "object", + "properties": { + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "statementId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_entities", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_entities", + "inputSchema": { + "type": "object", + "properties": { + "entityIds": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + } + }, + "required": [ + "entityIds" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "create_item", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/create_item", + "inputSchema": { + "type": "object", + "properties": { + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "idempotencyKey", + "authoredText", + "data" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "create_property", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/create_property", + "inputSchema": { + "type": "object", + "properties": { + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "idempotencyKey", + "authoredText", + "data" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "mutate_entity", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/mutate_entity", + "inputSchema": { + "type": "object", + "properties": { + "entityId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "mutations": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "const": "label" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "set-label" + }, + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "language", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "remove-label" + }, + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + } + }, + "required": [ + "type", + "language" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "description" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "set-description" + }, + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "language", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "remove-description" + }, + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + } + }, + "required": [ + "type", + "language" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "alias" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "add-alias" + }, + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "language", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "remove-alias" + }, + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "ordinal": { + "type": "integer", + "minimum": 0, + "maximum": 1000000 + } + }, + "required": [ + "type", + "language", + "ordinal" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "sitelink" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "set-sitelink" + }, + "site": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "value": { + "type": "object", + "properties": { + "site": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "badges": { + "type": "array", + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^Q[1-9][0-9]*$" + } + }, + "url": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "site", + "title", + "badges" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "site", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "remove-sitelink" + }, + "site": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "type", + "site" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "statement" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "add-statement" + }, + "statement": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "type": { + "const": "statement" + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "rank": { + "enum": [ + "preferred", + "normal", + "deprecated" + ] + }, + "mainsnak": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + }, + "qualifiers": { + "type": "object", + "maxProperties": 200, + "patternProperties": { + "^P[1-9][0-9]*$": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + }, + "qualifiers-order": { + "type": "array", + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + } + }, + "references": { + "type": "array", + "maxItems": 200, + "items": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "snaks": { + "type": "object", + "maxProperties": 200, + "patternProperties": { + "^P[1-9][0-9]*$": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + }, + "snaks-order": { + "type": "array", + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + } + } + }, + "required": [ + "hash", + "snaks", + "snaks-order" + ], + "additionalProperties": false + } + } + }, + "required": [ + "id", + "type", + "text", + "rank", + "mainsnak", + "qualifiers", + "qualifiers-order", + "references" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "statement" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "replace-statement" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "statement": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "type": { + "const": "statement" + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "rank": { + "enum": [ + "preferred", + "normal", + "deprecated" + ] + }, + "mainsnak": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + }, + "qualifiers": { + "type": "object", + "maxProperties": 200, + "patternProperties": { + "^P[1-9][0-9]*$": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + }, + "qualifiers-order": { + "type": "array", + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + } + }, + "references": { + "type": "array", + "maxItems": 200, + "items": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "snaks": { + "type": "object", + "maxProperties": 200, + "patternProperties": { + "^P[1-9][0-9]*$": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + }, + "snaks-order": { + "type": "array", + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + } + } + }, + "required": [ + "hash", + "snaks", + "snaks-order" + ], + "additionalProperties": false + } + } + }, + "required": [ + "id", + "type", + "text", + "rank", + "mainsnak", + "qualifiers", + "qualifiers-order", + "references" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "statementId", + "statement" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "remove-statement" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "type", + "statementId" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "rank" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "type": "object", + "properties": { + "type": { + "const": "set-statement-rank" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "rank": { + "enum": [ + "preferred", + "normal", + "deprecated" + ] + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "statementId", + "rank", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "qualifier" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "add-qualifier" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "snak": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "statementId", + "snak", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "remove-qualifier" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "ordinal": { + "type": "integer", + "minimum": 0, + "maximum": 1000000 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "statementId", + "property", + "ordinal", + "text" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "reference" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "add-reference" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "reference": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "snaks": { + "type": "object", + "maxProperties": 200, + "patternProperties": { + "^P[1-9][0-9]*$": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + }, + "snaks-order": { + "type": "array", + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + } + } + }, + "required": [ + "hash", + "snaks", + "snaks-order" + ], + "additionalProperties": false + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "statementId", + "reference", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "replace-reference" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "reference": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "snaks": { + "type": "object", + "maxProperties": 200, + "patternProperties": { + "^P[1-9][0-9]*$": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + }, + "snaks-order": { + "type": "array", + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + } + } + }, + "required": [ + "hash", + "snaks", + "snaks-order" + ], + "additionalProperties": false + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "statementId", + "hash", + "reference", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "remove-reference" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "statementId", + "hash", + "text" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + } + ] + } + } + }, + "required": [ + "entityId", + "expectedRevision", + "idempotencyKey", + "mutations" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "entity_revision", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/entity_revision", + "inputSchema": { + "type": "object", + "properties": { + "entityId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "entityId", + "revision" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "statement_revision", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/statement_revision", + "inputSchema": { + "type": "object", + "properties": { + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "statementId", + "revision" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "export_entity_json", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/export_entity_json", + "inputSchema": { + "type": "object", + "properties": { + "entityId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "entityId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "create_resource", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/create_resource", + "inputSchema": { + "type": "object", + "properties": { + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "idempotencyKey", + "data" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_resource", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_resource", + "inputSchema": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "resourceId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "update_resource", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/update_resource", + "inputSchema": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "patch": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "resourceId", + "expectedRevision", + "idempotencyKey", + "patch" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "delete_resource", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": true, + "idempotent": true + }, + "route": "/api/v1/operations/delete_resource", + "inputSchema": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "resourceId", + "expectedRevision", + "idempotencyKey" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "resource_history", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/resource_history", + "inputSchema": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "resourceId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "resource_revision", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/resource_revision", + "inputSchema": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "resourceId", + "revision" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "hydrate_resource", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/hydrate_resource", + "inputSchema": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "resourceId", + "revision" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "create_annotation", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/create_annotation", + "inputSchema": { + "type": "object", + "properties": { + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "idempotencyKey", + "data" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_annotation", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_annotation", + "inputSchema": { + "type": "object", + "properties": { + "annotationId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "annotationId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "update_annotation", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/update_annotation", + "inputSchema": { + "type": "object", + "properties": { + "annotationId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "patch": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "annotationId", + "expectedRevision", + "idempotencyKey", + "patch" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "delete_annotation", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": true, + "idempotent": true + }, + "route": "/api/v1/operations/delete_annotation", + "inputSchema": { + "type": "object", + "properties": { + "annotationId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "annotationId", + "expectedRevision", + "idempotencyKey" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "annotation_history", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/annotation_history", + "inputSchema": { + "type": "object", + "properties": { + "annotationId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "annotationId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "annotation_revision", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/annotation_revision", + "inputSchema": { + "type": "object", + "properties": { + "annotationId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "annotationId", + "revision" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "validate_sparql", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/validate_sparql", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "maxLength": 65536 + } + }, + "required": [ + "query" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "query_sparql", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/query_sparql", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "maxLength": 65536 + }, + "dryRun": { + "type": "boolean" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "query" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "list_tasks", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/list_tasks", + "inputSchema": { + "type": "object", + "properties": { + "cursor": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "state": { + "enum": [ + "unclaimed", + "claimed", + "completed", + "archived" + ] + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "search_tasks", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/search_tasks", + "inputSchema": { + "type": "object", + "properties": { + "text": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "cursor": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "text" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_task", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_task", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_task_packet", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_task_packet", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "create_task", + "version": 1, + "capability": "task:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/create_task", + "inputSchema": { + "type": "object", + "properties": { + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "prompt": { + "type": "string", + "minLength": 1, + "maxLength": 131072 + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "idempotencyKey", + "description", + "prompt" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "update_task", + "version": 1, + "capability": "task:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/update_task", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "leaseToken": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "patch": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "taskId", + "expectedRevision", + "patch" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "archive_task", + "version": 1, + "capability": "task:write", + "annotations": { + "readOnly": false, + "destructive": true, + "idempotent": true + }, + "route": "/api/v1/operations/archive_task", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "leaseToken": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "taskId", + "expectedRevision" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "claim_task", + "version": 1, + "capability": "task:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": false + }, + "route": "/api/v1/operations/claim_task", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "leaseSeconds": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "release_task", + "version": 1, + "capability": "task:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/release_task", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "leaseToken": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "taskId", + "leaseToken", + "expectedRevision", + "idempotencyKey" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "complete_task", + "version": 1, + "capability": "task:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/complete_task", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "leaseToken": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "result": { + "type": "string", + "minLength": 1, + "maxLength": 262144 + }, + "outcomeKind": { + "enum": [ + "success", + "negative", + "inconclusive" + ] + } + }, + "required": [ + "taskId", + "leaseToken", + "expectedRevision", + "idempotencyKey", + "result" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "task_history", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/task_history", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "list_memories", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/list_memories", + "inputSchema": { + "type": "object", + "properties": { + "cursor": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_memory", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_memory", + "inputSchema": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "slug" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "upsert_memory", + "version": 1, + "capability": "memory:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/upsert_memory", + "inputSchema": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "content": { + "type": "string", + "minLength": 1, + "maxLength": 131072 + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "slug", + "idempotencyKey", + "description", + "content" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "delete_memory", + "version": 1, + "capability": "memory:write", + "annotations": { + "readOnly": false, + "destructive": true, + "idempotent": true + }, + "route": "/api/v1/operations/delete_memory", + "inputSchema": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "slug", + "expectedRevision", + "idempotencyKey" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "memory_history", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/memory_history", + "inputSchema": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "slug" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "list_prompts", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/list_prompts", + "inputSchema": { + "type": "object", + "properties": { + "cursor": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_prompt", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_prompt", + "inputSchema": { + "type": "object", + "properties": { + "promptId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "promptId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "create_prompt", + "version": 1, + "capability": "prompt:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/create_prompt", + "inputSchema": { + "type": "object", + "properties": { + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "promptText": { + "type": "string", + "minLength": 1, + "maxLength": 131072 + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "idempotencyKey", + "name", + "promptText" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "update_prompt", + "version": 1, + "capability": "prompt:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/update_prompt", + "inputSchema": { + "type": "object", + "properties": { + "promptId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "patch": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "promptId", + "expectedRevision", + "idempotencyKey", + "patch" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "delete_prompt", + "version": 1, + "capability": "prompt:write", + "annotations": { + "readOnly": false, + "destructive": true, + "idempotent": true + }, + "route": "/api/v1/operations/delete_prompt", + "inputSchema": { + "type": "object", + "properties": { + "promptId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "promptId", + "expectedRevision", + "idempotencyKey" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "prompt_history", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/prompt_history", + "inputSchema": { + "type": "object", + "properties": { + "promptId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "promptId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + } + ], + "catalogDigest": "af455b4d6ab3560bca9c0ab48e9db7bd3cbcfa166ace875578d88d182069071f", + "legacyNames": { + "add_alias": "mutate_entity", + "add_reference": "mutate_entity", + "add_sitelink": "mutate_entity", + "add_statement": "mutate_entity", + "add_qualifier": "mutate_entity", + "create_entity": "create_item or create_property", + "delete_alias": "mutate_entity", + "delete_reference": "mutate_entity", + "delete_sitelink": "mutate_entity", + "delete_statement": "mutate_entity", + "delete_qualifier": "mutate_entity", + "dry_run_sparql": "query_sparql with dryRun=true", + "get_search_status": "search_admin with action=health", + "query_sparql_update": null, + "reset_abandoned_claim": null, + "search_status": "gnolith_status or search_admin with action=health", + "set_description": "mutate_entity", + "set_label": "mutate_entity", + "set_rank": "mutate_entity" + } +} diff --git a/schemas/legacy-operation-names-v1.json b/schemas/legacy-operation-names-v1.json new file mode 100644 index 0000000..0351a3e --- /dev/null +++ b/schemas/legacy-operation-names-v1.json @@ -0,0 +1,24 @@ +{ + "format": "gnolith-legacy-operation-names-v1", + "names": { + "add_alias": "mutate_entity", + "add_reference": "mutate_entity", + "add_sitelink": "mutate_entity", + "add_statement": "mutate_entity", + "add_qualifier": "mutate_entity", + "create_entity": "create_item or create_property", + "delete_alias": "mutate_entity", + "delete_reference": "mutate_entity", + "delete_sitelink": "mutate_entity", + "delete_statement": "mutate_entity", + "delete_qualifier": "mutate_entity", + "dry_run_sparql": "query_sparql with dryRun=true", + "get_search_status": "search_admin with action=health", + "query_sparql_update": null, + "reset_abandoned_claim": null, + "search_status": "gnolith_status or search_admin with action=health", + "set_description": "mutate_entity", + "set_label": "mutate_entity", + "set_rank": "mutate_entity" + } +} diff --git a/schemas/workshop-config-v1.json b/schemas/workshop-config-v1.json new file mode 100644 index 0000000..4807df5 --- /dev/null +++ b/schemas/workshop-config-v1.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://gnolith.dev/schemas/workshop-config-v1.json", + "title": "gnolith-workshop-config-v1", + "type": "object", + "additionalProperties": false, + "required": ["format", "listener", "authentication", "adapterModule"], + "properties": { + "format": { "const": "gnolith-workshop-config-v1" }, + "listener": { + "type": "object", + "additionalProperties": false, + "required": ["host", "port", "profile"], + "properties": { + "host": { "type": "string", "minLength": 1, "maxLength": 255 }, + "port": { "type": "integer", "minimum": 0, "maximum": 65535 }, + "profile": { "enum": ["container", "remote", "test"] } + } + }, + "authentication": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["profile", "credentialStore"], + "properties": { + "profile": { "const": "local-bearer-v1" }, + "credentialStore": { "$ref": "#/$defs/protectedSelector" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "profile", + "issuer", + "audience", + "algorithms", + "requiredScopes", + "resource", + "authorizationServers", + "scopeCapabilityMap", + "principalClaim", + "workspaceClaim", + "installationClaim", + "credentialClaim", + "maxClockSkewSeconds", + "metadata" + ], + "properties": { + "profile": { "const": "remote-oauth-v1" }, + "issuer": { "type": "string", "format": "uri" }, + "audience": { "type": "string", "minLength": 1 }, + "algorithms": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string" } + }, + "requiredScopes": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string" } + }, + "resource": { "type": "string", "format": "uri" }, + "authorizationServers": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string", "format": "uri" } + }, + "scopeCapabilityMap": { + "type": "object", + "additionalProperties": { "type": "string", "minLength": 1 } + }, + "principalClaim": { "type": "string", "minLength": 1 }, + "workspaceClaim": { "type": "string", "minLength": 1 }, + "installationClaim": { "type": "string", "minLength": 1 }, + "credentialClaim": { "type": "string", "minLength": 1 }, + "maxClockSkewSeconds": { + "type": "integer", + "minimum": 0, + "maximum": 300 + }, + "metadata": { "$ref": "#/$defs/protectedSelector" } + } + } + ] + }, + "adapterModule": { "type": "string", "minLength": 1 }, + "corsOrigins": { + "type": "array", + "uniqueItems": true, + "items": { "type": "string", "format": "uri" } + }, + "apiPrefix": { "const": "/api/v1" }, + "assetMount": { + "type": "object", + "additionalProperties": false, + "required": ["prefix", "manifest", "root"], + "properties": { + "prefix": { + "type": "string", + "pattern": "^/[a-z0-9][a-z0-9_-]*(?:/[a-z0-9][a-z0-9_-]*)*$", + "not": { + "pattern": "^/(?:mcp|health|api|\\.well-known)(?:/|$)" + }, + "description": "Static UI mount. Use /app. Reserved service prefixes and / are rejected.", + "examples": ["/app"] + }, + "manifest": { "$ref": "#/$defs/protectedSelector" }, + "root": { "$ref": "#/$defs/protectedSelector" } + } + }, + "background": { + "type": "object", + "additionalProperties": false, + "required": ["intervalMs", "timeoutMs"], + "properties": { + "intervalMs": { + "type": "integer", + "minimum": 100, + "maximum": 86400000 + }, + "timeoutMs": { + "type": "integer", + "minimum": 1, + "maximum": 86400000 + } + } + }, + "shutdownTimeoutMs": { + "type": "integer", + "minimum": 1, + "maximum": 300000 + } + }, + "$defs": { + "protectedSelector": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "value"], + "properties": { + "kind": { + "enum": ["file", "inherited-descriptor", "environment-selector"] + }, + "value": { "type": ["string", "integer"] } + } + } + } +} diff --git a/scripts/clean.mjs b/scripts/clean.mjs index e782526..95de02a 100644 --- a/scripts/clean.mjs +++ b/scripts/clean.mjs @@ -2,7 +2,7 @@ import { rmSync } from 'node:fs'; import { basename, dirname, resolve } from 'node:path'; const root = resolve('.'); -for (const name of ['dist', 'coverage', '.wrangler']) { +for (const name of ['dist', 'coverage']) { const target = resolve(root, name); if (dirname(target) !== root || basename(target) !== name) { throw new Error(`Unexpected clean target: ${target}`); diff --git a/scripts/consumer-smoke.mjs b/scripts/consumer-smoke.mjs deleted file mode 100644 index 2355c8d..0000000 --- a/scripts/consumer-smoke.mjs +++ /dev/null @@ -1,296 +0,0 @@ -import { execFileSync } from 'node:child_process'; -import { - existsSync, - mkdtempSync, - mkdirSync, - readFileSync, - readdirSync, - writeFileSync, -} from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { loadAndVerifyArtifact } from './artifact-provenance.mjs'; - -const npmCli = process.env.npm_execpath; -if (!npmCli) throw new Error('consumer:check must be run through npm'); -const projectRoot = process.cwd(); -const root = mkdtempSync(join(tmpdir(), 'workshop-consumer-')); -const source = JSON.parse(readFileSync('package.json', 'utf8')); -const publicVersion = process.env.WORKSHOP_PUBLIC_CONSUMER_VERSION; -const installTarget = publicVersion - ? `@gnolith/workshop@${publicVersion}` - : (await loadAndVerifyArtifact()).archive; -writeFileSync( - join(root, 'package.json'), - JSON.stringify({ - private: true, - type: 'module', - dependencies: { - '@gnolith/taproot': '0.4.0', - '@cloudflare/vite-plugin': '1.45.1', - '@vitejs/plugin-react': '6.0.3', - '@vitejs/plugin-rsc': '0.5.28', - react: '19.2.7', - 'react-dom': '19.2.7', - 'react-server-dom-webpack': '19.2.7', - vinext: '1.0.0-beta.2', - vite: '8.1.5', - wrangler: '4.112.0', - entities: '4.5.0', - typescript: '5.9.3', - }, - }), -); -execFileSync( - process.execPath, - [ - npmCli, - 'install', - '--ignore-scripts', - '--no-audit', - '--no-fund', - '--prefix', - root, - installTarget, - ], - { stdio: 'inherit' }, -); -writeFileSync( - join(root, 'smoke.mjs'), - ` - import { workshopPackage } from '@gnolith/workshop'; - import { createWorkshopCore, createWorkshopToolDispatcher } from '@gnolith/workshop/core'; - import { createWorkshopClient, taskState } from '@gnolith/workshop/protocol'; - import { createWorkshopRuntime, TaskService } from '@gnolith/workshop/server'; - import { createWorkshopMcpServer, workshopTools } from '@gnolith/workshop/mcp'; - import { createWorkshopMcpHandler } from '@gnolith/workshop/site'; - import { workshopMigrations } from '@gnolith/workshop/migrations'; - import { createWorkshopPlugin, workshopPlugin, TaskList, WorkshopTasksScreen } from '@gnolith/workshop/ui'; - const values = [workshopPackage, createWorkshopCore, createWorkshopToolDispatcher, - createWorkshopClient, taskState, createWorkshopRuntime, - TaskService, createWorkshopMcpServer, workshopTools, createWorkshopMcpHandler, - workshopMigrations, createWorkshopPlugin, workshopPlugin, TaskList, WorkshopTasksScreen]; - if (values.some((value) => value === undefined)) throw new Error('Public export missing'); - `, -); -execFileSync(process.execPath, [join(root, 'smoke.mjs')], { - cwd: root, - stdio: 'inherit', -}); -writeFileSync( - join(root, 'history-contract.ts'), - ` - import type { - HistoryRequestOptions, - MemoryRevision, - TaskRevision, - WorkshopClient, - } from '@gnolith/workshop/protocol'; - import type { MemoryService, TaskService } from '@gnolith/workshop/server'; - - declare const client: WorkshopClient; - declare const tasks: TaskService; - declare const memories: MemoryService; - declare const authorization: Parameters[1]; - const bounded: HistoryRequestOptions = { limit: 10 }; - const taskHistory: Promise = client.tasks.history('task-id', bounded); - const memoryHistory: Promise = client.memories.history('memory-slug', bounded); - const coreTaskHistory: Promise = tasks.history('task-id', authorization, bounded); - const coreMemoryHistory: Promise = memories.history('memory-slug', authorization, bounded); - void [taskHistory, memoryHistory, coreTaskHistory, coreMemoryHistory]; - `, -); -writeFileSync( - join(root, 'tsconfig.json'), - JSON.stringify({ - compilerOptions: { - target: 'ES2022', - module: 'ESNext', - moduleResolution: 'Bundler', - strict: true, - noEmit: true, - skipLibCheck: true, - }, - include: ['history-contract.ts'], - }), -); -execFileSync( - process.execPath, - [join(root, 'node_modules', 'typescript', 'bin', 'tsc')], - { cwd: root, stdio: 'inherit' }, -); -const packageRoot = join(root, 'node_modules', ...source.name.split('/')); -for (const path of [ - 'dist/index.d.ts', - 'dist/core.d.ts', - 'dist/protocol.d.ts', - 'dist/server.d.ts', - 'dist/mcp.d.ts', - 'dist/site.d.ts', - 'dist/ui.d.ts', - 'dist/migrations.d.ts', - 'dist/styles.css', - 'migrations/0001_workshop.sql', - 'migrations/0002_revisions.sql', - 'migrations/0003_resumable_onboarding.sql', - 'migrations/0004_authorization.sql', - 'SECURITY.md', -]) { - if (!existsSync(join(packageRoot, path))) { - throw new Error(`Packed package is missing ${path}`); - } -} -const packedPaths = readdirSync(packageRoot, { - recursive: true, - withFileTypes: true, -}).map((entry) => join(entry.parentPath, entry.name)); -for (const forbidden of ['.wrangler', join('dist', 'server', 'diamond.js')]) { - if (packedPaths.some((path) => path.includes(forbidden))) { - throw new Error( - `Packed package contains forbidden stale output: ${forbidden}`, - ); - } -} -const canaryRoot = join(root, 'package-runtime-canary'); -writeFileSync( - join(root, 'worker.ts'), - readFileSync('examples/package-runtime-canary/worker.ts', 'utf8'), -); -writeFileSync( - join(root, 'wrangler.jsonc'), - readFileSync('examples/package-runtime-canary/wrangler.jsonc', 'utf8'), -); -const wranglerOutput = execFileSync( - process.execPath, - [ - join(projectRoot, 'node_modules', 'wrangler', 'bin', 'wrangler.js'), - 'deploy', - '--dry-run', - '--config', - join(root, 'wrangler.jsonc'), - '--outdir', - canaryRoot, - ], - { cwd: root, encoding: 'utf8' }, -); -if (/node:events|nodejs_compat/iu.test(wranglerOutput)) { - throw new Error( - 'Isolated exact-tarball Worker consumer pulled Node compatibility requirements', - ); -} -if (!existsSync(join(canaryRoot, 'worker.js'))) { - throw new Error( - 'Isolated exact-tarball Worker consumer did not produce a bundle', - ); -} - -const frontendRoot = mkdtempSync(join(tmpdir(), 'workshop-vinext-consumer-')); -writeFileSync( - join(frontendRoot, 'package.json'), - JSON.stringify({ - private: true, - type: 'module', - dependencies: { - '@cloudflare/vite-plugin': '1.45.1', - '@vitejs/plugin-react': '6.0.3', - '@vitejs/plugin-rsc': '0.5.28', - react: '19.2.7', - 'react-dom': '19.2.7', - 'react-server-dom-webpack': '19.2.7', - vinext: '1.0.0-beta.2', - vite: '8.1.5', - }, - }), -); -execFileSync( - process.execPath, - [ - npmCli, - 'install', - '--ignore-scripts', - '--no-audit', - '--no-fund', - '--prefix', - frontendRoot, - installTarget, - ], - { stdio: 'inherit' }, -); -const appRoot = join(frontendRoot, 'app'); -const mcpRouteRoot = join(appRoot, 'api', 'workshop', 'mcp'); -mkdirSync(mcpRouteRoot, { recursive: true }); -writeFileSync( - join(appRoot, 'layout.tsx'), - ` - import type { ReactNode } from 'react'; - import '@gnolith/workshop/styles.css'; - export default function Layout({ children }: { children: ReactNode }) { - return {children}; - } - `, -); -writeFileSync( - join(appRoot, 'page.tsx'), - ` - import { McpStatusPanel, TaskList, WorkshopDashboard } from '@gnolith/workshop/ui'; - const task = { - id: 'canary-task', description: 'Canary task', prompt: 'Verify Workshop UI.', - contextQueries: [], memorySlugs: [], claimed: false, - installationId: 'canary:installation', ownerPrincipalId: 'canary:verifier', - workspaceId: 'canary:workspace', visibility: { version: 1, clauses: [] }, - authorizationRevision: 1, - createdAt: '2026-07-20T00:00:00.000Z', updatedAt: '2026-07-20T00:00:00.000Z', - }; - export default function Page() { - return
-
; - } - `, -); -writeFileSync( - join(mcpRouteRoot, 'route.ts'), - ` - const handler = () => new Response('Workshop UI package canary'); - export const GET = handler; - export const POST = handler; - `, -); -writeFileSync( - join(frontendRoot, 'tsconfig.json'), - JSON.stringify({ - compilerOptions: { - target: 'ES2022', - module: 'ESNext', - moduleResolution: 'Bundler', - jsx: 'react-jsx', - strict: true, - skipLibCheck: true, - }, - include: ['app'], - }), -); -writeFileSync( - join(frontendRoot, 'vite.config.ts'), - ` - import { cloudflare } from '@cloudflare/vite-plugin'; - import { defineConfig } from 'vite'; - import vinext from 'vinext'; - export default defineConfig({ - plugins: [ - vinext(), - cloudflare({ viteEnvironment: { name: 'rsc', childEnvironments: ['ssr'] } }), - ], - }); - `, -); -execFileSync( - process.execPath, - [join(frontendRoot, 'node_modules', 'vinext', 'dist', 'cli.js'), 'build'], - { cwd: frontendRoot, stdio: 'inherit' }, -); -if (!existsSync(join(frontendRoot, 'dist'))) { - throw new Error('Isolated vinext exact-tarball consumer did not build'); -} -console.log( - `generic declaration/runtime, Worker, and vinext isolated package consumers passed for ${source.name}@${publicVersion ?? source.version}`, -); diff --git a/scripts/copy-assets.mjs b/scripts/copy-assets.mjs deleted file mode 100644 index 69b754d..0000000 --- a/scripts/copy-assets.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import { copyFile, mkdir } from 'node:fs/promises'; - -await mkdir(new URL('../dist/', import.meta.url), { recursive: true }); -await copyFile( - new URL('../src/ui/styles/workshop.css', import.meta.url), - new URL('../dist/styles.css', import.meta.url), -); diff --git a/scripts/dev-security-smoke.mjs b/scripts/dev-security-smoke.mjs index b1ddd48..6cdb0e6 100644 --- a/scripts/dev-security-smoke.mjs +++ b/scripts/dev-security-smoke.mjs @@ -1,180 +1,58 @@ import { Buffer } from 'node:buffer'; -import { readFile } from 'node:fs/promises'; -import { createServer, request } from 'node:http'; import { createRequire } from 'node:module'; -import { dirname, join } from 'node:path'; -import { clearTimeout, setTimeout } from 'node:timers'; import { pathToFileURL } from 'node:url'; -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { createGnolithMcpServer } from '../dist/mcp/server.js'; -const require = createRequire(import.meta.url); -const LOOPBACK_TIMEOUT_MS = 2_000; const MAX_RESPONSE_BYTES = 64 * 1024; -async function packageMetadata(entry) { - let directory = dirname(entry); - for (;;) { - try { - return JSON.parse( - await readFile(join(directory, 'package.json'), 'utf8'), - ); - } catch (error) { - const parent = dirname(directory); - if (parent === directory) { - throw error; - } - directory = parent; - } - } -} - -const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, +const mcp = createGnolithMcpServer({ + version: '0.5.0', + operations: { + async execute() { + throw new Error('invalid requests must not dispatch'); + }, + }, + authenticate: async () => ({}), + maxRequestBytes: MAX_RESPONSE_BYTES, }); -if (typeof transport.handleRequest !== 'function') { +const headers = { + accept: 'application/json, text/event-stream', + 'content-type': 'application/json', +}; +const malformed = await mcp.handle( + new Request('http://127.0.0.1/mcp', { + method: 'POST', + headers, + body: '{', + }), +); +const payload = await malformed.json(); +if ( + malformed.status !== 400 || + !malformed.headers.get('content-type')?.startsWith('application/json') || + payload?.jsonrpc !== '2.0' || + payload?.error?.code !== -32700 +) { throw new Error( - 'MCP StreamableHTTPServerTransport did not construct correctly', + `Workshop MCP returned status=${malformed.status}, contentType=${malformed.headers.get('content-type')}, code=${payload?.error?.code}`, ); } -const server = createServer((incoming, response) => { - if (incoming.url === '/oversized') { - response.end(Buffer.alloc(MAX_RESPONSE_BYTES + 1)); - return; - } - void transport.handleRequest(incoming, response).catch((error) => { - response.destroy(error); - }); -}); -server.requestTimeout = LOOPBACK_TIMEOUT_MS; -server.headersTimeout = LOOPBACK_TIMEOUT_MS; -server.keepAliveTimeout = 500; -server.setTimeout(LOOPBACK_TIMEOUT_MS, (socket) => { - socket.destroy(new Error('MCP compatibility server request timed out')); -}); -await new Promise((resolve, reject) => { - server.once('error', reject); - server.listen(0, '127.0.0.1', resolve); -}); - -try { - const address = server.address(); - if (address === null || typeof address === 'string') { - throw new Error('MCP compatibility server did not bind to a TCP port'); - } - - const requestLoopback = (path, body = '') => - new Promise((resolve, reject) => { - const controller = new AbortController(); - const timeout = setTimeout(() => { - controller.abort( - new Error( - `MCP compatibility request exceeded ${LOOPBACK_TIMEOUT_MS}ms`, - ), - ); - }, LOOPBACK_TIMEOUT_MS); - let settled = false; - const settle = (callback, value) => { - if (settled) return; - settled = true; - clearTimeout(timeout); - callback(value); - }; - - const outgoing = request( - { - host: '127.0.0.1', - port: address.port, - path, - method: 'POST', - signal: controller.signal, - headers: { - accept: 'application/json, text/event-stream', - 'content-type': 'application/json', - 'content-length': Buffer.byteLength(body), - }, - }, - (response) => { - const chunks = []; - let responseBytes = 0; - response.on('data', (chunk) => { - responseBytes += chunk.length; - if (responseBytes > MAX_RESPONSE_BYTES) { - const error = new Error( - `MCP compatibility response exceeded ${MAX_RESPONSE_BYTES} bytes`, - ); - controller.abort(error); - response.destroy(error); - outgoing.destroy(error); - settle(reject, error); - return; - } - chunks.push(chunk); - }); - response.on('end', () => { - settle(resolve, { - status: response.statusCode, - contentType: response.headers['content-type'], - body: Buffer.concat(chunks).toString('utf8'), - }); - }); - response.once('error', (error) => settle(reject, error)); - }, - ); - outgoing.once('error', (error) => settle(reject, error)); - outgoing.setTimeout(LOOPBACK_TIMEOUT_MS, () => { - const error = new Error( - `MCP compatibility socket exceeded ${LOOPBACK_TIMEOUT_MS}ms`, - ); - outgoing.destroy(error); - settle(reject, error); - }); - outgoing.end(body); - }); - - const result = await requestLoopback('/mcp', '{'); - - const payload = JSON.parse(result.body); - if ( - result.status !== 400 || - !result.contentType?.startsWith('application/json') || - payload?.jsonrpc !== '2.0' || - payload?.error?.code !== -32700 - ) { - throw new Error( - `MCP Node-to-Web HTTP bridge returned status=${result.status}, contentType=${result.contentType}, code=${payload?.error?.code}`, - ); - } - - await requestLoopback('/oversized').then( - () => { - throw new Error('oversized MCP compatibility response was accepted'); - }, - (error) => { - if (!String(error.message).includes('exceeded 65536 bytes')) { - throw error; - } +const oversized = await mcp.handle( + new Request('http://127.0.0.1/mcp', { + method: 'POST', + headers: { + ...headers, + 'content-length': String(MAX_RESPONSE_BYTES + 1), }, + body: Buffer.alloc(MAX_RESPONSE_BYTES + 1, 0x20), + }), +); +if (oversized.status !== 413) { + throw new Error( + `oversized Workshop MCP request returned ${oversized.status}`, ); -} finally { - try { - await transport.close(); - } finally { - server.closeAllConnections(); - await new Promise((resolve, reject) => { - server.close((error) => - error === undefined || error.code === 'ERR_SERVER_NOT_RUNNING' - ? resolve() - : reject(error), - ); - }); - } -} - -const hono = await packageMetadata(require.resolve('@hono/node-server')); -if (hono.name !== '@hono/node-server' || hono.version !== '2.0.11') { - throw new Error(`unexpected @hono/node-server version: ${hono.version}`); } const miniflareRequire = createRequire(import.meta.resolve('miniflare')); @@ -207,5 +85,5 @@ if ( } console.log( - `dev security compatibility passed (@hono/node-server ${hono.version}, sharp ${sharp.versions.sharp})`, + `dev security compatibility passed (Workshop MCP, sharp ${sharp.versions.sharp})`, ); diff --git a/scripts/generate-operation-contract.mjs b/scripts/generate-operation-contract.mjs new file mode 100644 index 0000000..6d75082 --- /dev/null +++ b/scripts/generate-operation-contract.mjs @@ -0,0 +1,1383 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { format as formatWithPrettier } from 'prettier'; + +const version = 2; +const routePrefix = '/api/v1/operations'; +const read = { readOnly: true, destructive: false, idempotent: true }; +const write = { readOnly: false, destructive: false, idempotent: false }; +const idempotentWrite = { + readOnly: false, + destructive: false, + idempotent: true, +}; +const destructive = { + readOnly: false, + destructive: true, + idempotent: true, +}; +const object = (properties = {}, required = []) => ({ + type: 'object', + properties, + ...(required.length ? { required } : {}), + additionalProperties: false, +}); +const text = (maxLength = 4096) => ({ + type: 'string', + minLength: 1, + maxLength, +}); +const positive = { type: 'integer', minimum: 1, maximum: 1_000_000 }; +const id = text(256); +const idInput = (field = 'id') => object({ [field]: id }, [field]); +const revisionInput = (field = 'id') => + object({ [field]: id, revision: positive }, [field, 'revision']); +const output = object({}, []); +output.additionalProperties = true; + +const definitions = []; +function operation( + name, + capability, + annotations, + inputSchema = object(), + outputSchema = output, +) { + definitions.push({ + name, + version: 1, + capability, + annotations, + route: `${routePrefix}/${name}`, + inputSchema, + outputSchema, + }); +} + +operation( + 'gnolith_status', + 'read', + read, + object(), + object( + { + installationId: id, + baseIri: text(4096), + principalId: id, + credentialId: id, + activeWorkspaceId: { + anyOf: [id, { type: 'null' }], + }, + capabilities: { + type: 'array', + maxItems: 256, + uniqueItems: true, + items: text(256), + }, + authorizationRevision: { + type: 'integer', + minimum: 0, + maximum: 1_000_000_000, + }, + migrationReadiness: object( + { + namespace: { const: '@gnolith/workshop' }, + version: positive, + ready: { type: 'boolean' }, + }, + ['namespace', 'version', 'ready'], + ), + compatibility: object( + { + diamond: { type: 'boolean' }, + taproot: { type: 'boolean' }, + }, + ['diamond', 'taproot'], + ), + canonicalReady: { type: 'boolean' }, + authorizationReady: { type: 'boolean' }, + lexicalReady: { type: 'boolean' }, + semanticState: object( + { + state: { enum: ['ready', 'degraded', 'unconfigured'] }, + configured: { type: 'boolean' }, + revision: { + anyOf: [positive, { type: 'null' }], + }, + fingerprint: { + anyOf: [ + { type: 'string', pattern: '^[a-f0-9]{64}$' }, + { type: 'null' }, + ], + }, + ready: { type: 'boolean' }, + diagnostic: { + anyOf: [ + object( + { + format: { + const: 'gnolith-workshop-semantic-diagnostic-v1', + }, + version: { const: 1 }, + code: { + enum: ['materialization-pending', 'provider-unavailable'], + }, + retryable: { type: 'boolean' }, + backend: { enum: ['sqlite', 'qdrant'] }, + repair: { const: 'retry' }, + }, + ['format', 'version', 'code', 'retryable', 'backend', 'repair'], + ), + { type: 'null' }, + ], + }, + }, + [ + 'state', + 'configured', + 'revision', + 'fingerprint', + 'ready', + 'diagnostic', + ], + ), + producers: object( + { + ready: { type: 'boolean' }, + fingerprint: text(256), + kinds: { + type: 'array', + minItems: 3, + maxItems: 3, + uniqueItems: true, + items: { enum: ['task', 'memory', 'prompt'] }, + }, + }, + ['ready', 'fingerprint', 'kinds'], + ), + blobReady: { type: 'boolean' }, + versions: object( + { + server: text(128), + operationSchema: { const: version }, + }, + ['server', 'operationSchema'], + ), + operationCatalogDigest: { + type: 'string', + pattern: '^[a-f0-9]{64}$', + }, + }, + [ + 'installationId', + 'baseIri', + 'principalId', + 'credentialId', + 'activeWorkspaceId', + 'capabilities', + 'authorizationRevision', + 'migrationReadiness', + 'compatibility', + 'canonicalReady', + 'authorizationReady', + 'lexicalReady', + 'semanticState', + 'producers', + 'blobReady', + 'versions', + 'operationCatalogDigest', + ], + ), +); +const policyRevision = { + type: 'integer', + minimum: 1, + maximum: 1_000_000_000, +}; +const digest = { type: 'string', pattern: '^[a-f0-9]{64}$' }; +const localBearerTokenDigest = { + ...digest, + description: + 'Lowercase hexadecimal SHA-256 of UTF-8 "gnolith-local-bearer-v1\\u0000" followed by the 32 bytes obtained by base64url-decoding the exact 43-character bearer token.', +}; +const visibilityAtom = { + description: + 'One alternative inside a visibility clause. Atoms in the same clause are ORed.', + oneOf: [ + object({ kind: { const: 'public' } }, ['kind']), + object({ kind: { const: 'principal' }, principalId: id }, [ + 'kind', + 'principalId', + ]), + object({ kind: { const: 'workspace' }, workspaceId: id }, [ + 'kind', + 'workspaceId', + ]), + object({ kind: { const: 'capability' }, capability: text(256) }, [ + 'kind', + 'capability', + ]), + ], +}; +const visibilityScope = object( + { + version: { const: 1 }, + clauses: { + description: + 'Canonical CNF visibility: clauses are ANDed; atoms within each clause are ORed. The resulting scope must retain the administering principal.', + type: 'array', + maxItems: 32, + items: { + type: 'array', + minItems: 1, + maxItems: 32, + items: visibilityAtom, + }, + }, + }, + ['version', 'clauses'], +); +const policyTarget = object( + { + kind: { enum: ['entity', 'resource', 'annotation'] }, + id, + }, + ['kind', 'id'], +); +const authorizationMutation = (action, properties, required) => + object( + { + version: { const: 1 }, + action: { const: action }, + expectedPolicyRevision: policyRevision, + idempotencyKey: id, + ...properties, + }, + [ + 'version', + 'action', + 'expectedPolicyRevision', + 'idempotencyKey', + ...required, + ], + ); +operation( + 'authorization_admin', + 'admin', + idempotentWrite, + { + oneOf: [ + authorizationMutation( + 'principal-create', + { + principal: object( + { + principalId: id, + enabled: { type: 'boolean' }, + }, + ['principalId'], + ), + }, + ['principal'], + ), + ...['principal-enable', 'principal-disable', 'principal-revoke'].map( + (action) => + authorizationMutation(action, { principalId: id }, ['principalId']), + ), + ...['workspace-membership-add', 'workspace-membership-remove'].map( + (action) => + authorizationMutation(action, { principalId: id, workspaceId: id }, [ + 'principalId', + 'workspaceId', + ]), + ), + ...['workspace-grant-add', 'workspace-grant-remove'].map((action) => + authorizationMutation( + action, + { principalId: id, workspaceId: id, capability: text(256) }, + ['principalId', 'workspaceId', 'capability'], + ), + ), + authorizationMutation( + 'credential-register', + { + credential: object( + { + credentialId: id, + tokenDigest: localBearerTokenDigest, + principalId: id, + workspaceId: id, + }, + ['credentialId', 'tokenDigest', 'principalId', 'workspaceId'], + ), + }, + ['credential'], + ), + authorizationMutation('credential-revoke', { credentialId: id }, [ + 'credentialId', + ]), + authorizationMutation( + 'oauth-bind', + { + binding: object( + { + issuer: text(4096), + subject: text(1024), + principalId: id, + workspaceId: id, + credentialId: id, + }, + ['issuer', 'subject', 'principalId', 'workspaceId'], + ), + }, + ['binding'], + ), + authorizationMutation( + 'oauth-unbind', + { issuer: text(4096), subject: text(1024) }, + ['issuer', 'subject'], + ), + authorizationMutation( + 'visibility-policy-set', + { + target: policyTarget, + expectedTargetRevision: positive, + visibility: visibilityScope, + }, + ['target', 'expectedTargetRevision', 'visibility'], + ), + object( + { + version: { const: 1 }, + action: { const: 'status' }, + target: policyTarget, + }, + ['version', 'action'], + ), + object( + { + version: { const: 1 }, + action: { const: 'effective-policy' }, + target: policyTarget, + }, + ['version', 'action', 'target'], + ), + ], + }, + { + oneOf: [ + object( + { + version: { const: 1 }, + action: text(64), + status: { enum: ['committed', 'replayed'] }, + policyRevision, + auditId: id, + target: object({ kind: text(64), id }, ['kind', 'id']), + }, + ['version', 'action', 'status', 'policyRevision', 'auditId', 'target'], + ), + object( + { + version: { const: 1 }, + action: { enum: ['status', 'effective-policy'] }, + policyRevision, + value: { type: 'object', additionalProperties: true }, + }, + ['version', 'action', 'policyRevision', 'value'], + ), + ], + }, +); +operation( + 'search', + 'read', + read, + object( + { + text: text(16_384), + kinds: { + type: 'array', + maxItems: 7, + uniqueItems: true, + items: { + enum: [ + 'statement', + 'item', + 'task', + 'memory', + 'prompt', + 'resource', + 'annotation', + ], + }, + }, + cursor: text(4096), + limit: positive, + }, + ['text'], + ), +); +operation( + 'search_hydrate', + 'read', + read, + object( + { + kind: { + enum: [ + 'statement', + 'item', + 'task', + 'memory', + 'prompt', + 'resource', + 'annotation', + ], + }, + id, + revision: positive, + entityType: { enum: ['item', 'property'] }, + }, + ['kind', 'id', 'revision'], + ), +); +const semanticCredentialSelector = { + anyOf: [id, { type: 'null' }], +}; +const semanticEndpoint = { + type: 'string', + pattern: '^https?://', + maxLength: 4096, +}; +const semanticProviderEndpoint = { + ...semanticEndpoint, + description: + 'Exact embeddings POST URL: openai-compatible ends in /embeddings and ollama-compatible ends in /api/embed.', +}; +const semanticProvider = object( + { + kind: { enum: ['openai-compatible', 'ollama-compatible'] }, + endpoint: semanticProviderEndpoint, + model: text(512), + dimensions: { + type: 'integer', + minimum: 1, + maximum: 65_536, + }, + metric: { enum: ['cosine', 'dot', 'euclid'] }, + credentialSelector: semanticCredentialSelector, + allowPrivateEndpoint: { type: 'boolean' }, + redirectPolicy: { const: 'error' }, + }, + [ + 'kind', + 'endpoint', + 'model', + 'dimensions', + 'metric', + 'credentialSelector', + 'allowPrivateEndpoint', + 'redirectPolicy', + ], +); +const semanticVector = { + oneOf: [ + object({ kind: { const: 'sqlite' } }, ['kind']), + object( + { + kind: { const: 'qdrant' }, + endpoint: semanticEndpoint, + collection: text(256), + credentialSelector: semanticCredentialSelector, + allowPrivateEndpoint: { type: 'boolean' }, + redirectPolicy: { const: 'error' }, + }, + [ + 'kind', + 'endpoint', + 'collection', + 'credentialSelector', + 'allowPrivateEndpoint', + 'redirectPolicy', + ], + ), + ], +}; +const semanticConfiguration = object( + { + version: { const: 1 }, + id, + name: text(1024), + provider: semanticProvider, + vector: semanticVector, + }, + ['version', 'id', 'name', 'provider', 'vector'], +); +operation('search_admin', 'search:admin', write, { + oneOf: [ + object( + { + action: { const: 'configure' }, + options: object( + { + expectedRevision: { + type: 'integer', + minimum: 0, + maximum: Number.MAX_SAFE_INTEGER, + }, + idempotencyKey: id, + configuration: semanticConfiguration, + }, + ['expectedRevision', 'idempotencyKey', 'configuration'], + ), + }, + ['action', 'options'], + ), + object( + { + action: { + enum: [ + 'health', + 'materialize', + 'retry-dead', + 'rebuild', + 'activate', + 'semantic-select', + 'semantic-reconnect', + 'semantic-estimate', + 'semantic-approve', + 'semantic-run', + 'semantic-resume', + 'semantic-pause', + 'semantic-stop', + 'semantic-retry', + 'semantic-exclude', + 'semantic-retire', + 'semantic-delete', + ], + }, + limit: positive, + cursor: text(4096), + options: { type: 'object', additionalProperties: true }, + }, + ['action'], + ), + ], +}); + +for (const name of ['get_entity', 'entity_history', 'statement_history']) + operation( + name, + 'read', + read, + idInput(name === 'statement_history' ? 'statementId' : 'entityId'), + ); +operation( + 'get_entities', + 'read', + read, + object( + { + entityIds: { + type: 'array', + minItems: 1, + maxItems: 200, + uniqueItems: true, + items: id, + }, + }, + ['entityIds'], + ), +); +for (const name of ['create_item', 'create_property']) + operation( + name, + 'knowledge:write', + idempotentWrite, + object( + { + idempotencyKey: id, + authoredText: text(16_384), + data: { type: 'object', additionalProperties: true }, + }, + ['idempotencyKey', 'authoredText', 'data'], + ), + ); + +const propertyId = { type: 'string', pattern: '^P[1-9][0-9]*$' }; +const statementId = text(256); +const rank = { enum: ['preferred', 'normal', 'deprecated'] }; +const entityDatatype = { + enum: [ + 'wikibase-item', + 'wikibase-property', + 'wikibase-lexeme', + 'wikibase-form', + 'wikibase-sense', + 'entity-schema', + 'string', + 'external-id', + 'url', + 'commonsMedia', + 'monolingualtext', + 'time', + 'quantity', + 'globe-coordinate', + 'math', + 'musical-notation', + 'geo-shape', + 'tabular-data', + ], +}; +const entityValue = object( + { + 'entity-type': { + enum: ['item', 'property', 'lexeme', 'form', 'sense', 'entity-schema'], + }, + 'numeric-id': { type: 'integer', minimum: 1 }, + id: { + type: 'string', + pattern: + '^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$', + }, + }, + ['entity-type', 'id'], +); +const monolingualValue = object({ language: text(64), text: text(16_384) }, [ + 'language', + 'text', +]); +const timeValue = object( + { + time: text(256), + timezone: { type: 'integer' }, + before: { type: 'integer', minimum: 0 }, + after: { type: 'integer', minimum: 0 }, + precision: { type: 'integer', minimum: 0 }, + calendarmodel: text(4096), + }, + ['time', 'timezone', 'before', 'after', 'precision', 'calendarmodel'], +); +const quantityValue = object( + { + amount: text(256), + unit: text(4096), + lowerBound: text(256), + upperBound: text(256), + }, + ['amount', 'unit'], +); +const coordinateValue = object( + { + latitude: { type: 'number', minimum: -90, maximum: 90 }, + longitude: { type: 'number', minimum: -180, maximum: 180 }, + altitude: { anyOf: [{ type: 'number' }, { type: 'null' }] }, + precision: { + anyOf: [{ type: 'number', exclusiveMinimum: 0 }, { type: 'null' }], + }, + globe: text(4096), + }, + ['latitude', 'longitude', 'altitude', 'precision', 'globe'], +); +const dataValue = object( + { + value: { + anyOf: [ + text(1_000_000), + entityValue, + monolingualValue, + timeValue, + quantityValue, + coordinateValue, + ], + }, + type: text(128), + }, + ['value', 'type'], +); +const snak = { + oneOf: [ + object( + { + snaktype: { const: 'value' }, + property: propertyId, + hash: text(256), + datatype: entityDatatype, + datavalue: dataValue, + }, + ['snaktype', 'property', 'datatype', 'datavalue'], + ), + object( + { + snaktype: { enum: ['somevalue', 'novalue'] }, + property: propertyId, + hash: text(256), + datatype: entityDatatype, + }, + ['snaktype', 'property', 'datatype'], + ), + ], +}; +const snakMap = { + type: 'object', + maxProperties: 200, + patternProperties: { + '^P[1-9][0-9]*$': { + type: 'array', + minItems: 1, + maxItems: 200, + items: snak, + }, + }, + additionalProperties: false, +}; +const propertyOrder = { + type: 'array', + maxItems: 200, + uniqueItems: true, + items: propertyId, +}; +const reference = object( + { hash: text(256), snaks: snakMap, 'snaks-order': propertyOrder }, + ['hash', 'snaks', 'snaks-order'], +); +const statement = object( + { + id: statementId, + type: { const: 'statement' }, + text: text(16_384), + rank, + mainsnak: snak, + qualifiers: snakMap, + 'qualifiers-order': propertyOrder, + references: { + type: 'array', + maxItems: 200, + items: reference, + }, + }, + [ + 'id', + 'type', + 'text', + 'rank', + 'mainsnak', + 'qualifiers', + 'qualifiers-order', + 'references', + ], +); +const sitelink = object( + { + site: text(256), + title: text(16_384), + badges: { + type: 'array', + maxItems: 200, + uniqueItems: true, + items: { type: 'string', pattern: '^Q[1-9][0-9]*$' }, + }, + url: text(4096), + }, + ['site', 'title', 'badges'], +); +const command = (type, properties = {}, required = []) => + object({ type: { const: type }, ...properties }, ['type', ...required]); +const mutation = (kind, value) => + object({ kind: { const: kind }, authoredText: text(16_384), value }, [ + 'kind', + 'authoredText', + 'value', + ]); +const entityMutation = { + oneOf: [ + mutation('label', { + oneOf: [ + command('set-label', { language: text(64), value: text(16_384) }, [ + 'language', + 'value', + ]), + command('remove-label', { language: text(64) }, ['language']), + ], + }), + mutation('description', { + oneOf: [ + command( + 'set-description', + { language: text(64), value: text(16_384) }, + ['language', 'value'], + ), + command('remove-description', { language: text(64) }, ['language']), + ], + }), + mutation('alias', { + oneOf: [ + command('add-alias', { language: text(64), value: text(16_384) }, [ + 'language', + 'value', + ]), + command( + 'remove-alias', + { + language: text(64), + ordinal: { type: 'integer', minimum: 0, maximum: 1_000_000 }, + }, + ['language', 'ordinal'], + ), + ], + }), + mutation('sitelink', { + oneOf: [ + command('set-sitelink', { site: text(256), value: sitelink }, [ + 'site', + 'value', + ]), + command('remove-sitelink', { site: text(256) }, ['site']), + ], + }), + mutation('statement', { + oneOf: [ + command('add-statement', { statement }, ['statement']), + command('replace-statement', { statementId, statement }, [ + 'statementId', + 'statement', + ]), + command('remove-statement', { statementId }, ['statementId']), + ], + }), + mutation( + 'rank', + command('set-statement-rank', { statementId, rank, text: text(16_384) }, [ + 'statementId', + 'rank', + 'text', + ]), + ), + mutation('qualifier', { + oneOf: [ + command('add-qualifier', { statementId, snak, text: text(16_384) }, [ + 'statementId', + 'snak', + 'text', + ]), + command( + 'remove-qualifier', + { + statementId, + property: propertyId, + ordinal: { type: 'integer', minimum: 0, maximum: 1_000_000 }, + text: text(16_384), + }, + ['statementId', 'property', 'ordinal', 'text'], + ), + ], + }), + mutation('reference', { + oneOf: [ + command( + 'add-reference', + { statementId, reference, text: text(16_384) }, + ['statementId', 'reference', 'text'], + ), + command( + 'replace-reference', + { statementId, hash: text(256), reference, text: text(16_384) }, + ['statementId', 'hash', 'reference', 'text'], + ), + command( + 'remove-reference', + { statementId, hash: text(256), text: text(16_384) }, + ['statementId', 'hash', 'text'], + ), + ], + }), + ], +}; +operation( + 'mutate_entity', + 'knowledge:write', + idempotentWrite, + object( + { + entityId: id, + expectedRevision: positive, + idempotencyKey: id, + mutations: { + type: 'array', + minItems: 1, + maxItems: 200, + items: entityMutation, + }, + }, + ['entityId', 'expectedRevision', 'idempotencyKey', 'mutations'], + ), +); +for (const name of ['entity_revision', 'statement_revision']) + operation( + name, + 'read', + read, + revisionInput(name === 'statement_revision' ? 'statementId' : 'entityId'), + ); +operation('export_entity_json', 'read', read, idInput('entityId')); + +for (const domain of ['resource', 'annotation']) { + operation( + `create_${domain}`, + 'knowledge:write', + idempotentWrite, + object( + { + idempotencyKey: id, + data: { type: 'object', additionalProperties: true }, + }, + ['idempotencyKey', 'data'], + ), + ); + operation(`get_${domain}`, 'read', read, idInput(`${domain}Id`)); + operation( + `update_${domain}`, + 'knowledge:write', + idempotentWrite, + object( + { + [`${domain}Id`]: id, + expectedRevision: positive, + idempotencyKey: id, + patch: { type: 'object', additionalProperties: true }, + }, + [`${domain}Id`, 'expectedRevision', 'idempotencyKey', 'patch'], + ), + ); + operation( + `delete_${domain}`, + 'knowledge:write', + destructive, + object( + { + [`${domain}Id`]: id, + expectedRevision: positive, + idempotencyKey: id, + }, + [`${domain}Id`, 'expectedRevision', 'idempotencyKey'], + ), + ); + operation(`${domain}_history`, 'read', read, idInput(`${domain}Id`)); + operation(`${domain}_revision`, 'read', read, revisionInput(`${domain}Id`)); + if (domain === 'resource') + operation('hydrate_resource', 'read', read, revisionInput('resourceId')); +} + +operation( + 'validate_sparql', + 'read', + read, + object({ query: text(65_536) }, ['query']), +); +operation( + 'query_sparql', + 'read', + read, + object( + { + query: text(65_536), + dryRun: { type: 'boolean' }, + limit: positive, + }, + ['query'], + ), +); + +operation( + 'list_tasks', + 'read', + read, + object({ + cursor: text(4096), + limit: positive, + state: { enum: ['unclaimed', 'claimed', 'completed', 'archived'] }, + text: text(4096), + }), +); +operation( + 'search_tasks', + 'read', + read, + object({ text: text(4096), cursor: text(4096), limit: positive }, ['text']), +); +operation('get_task', 'read', read, idInput('taskId')); +operation('get_task_packet', 'read', read, idInput('taskId')); +operation( + 'create_task', + 'task:write', + idempotentWrite, + object( + { + idempotencyKey: id, + description: text(4096), + prompt: text(131_072), + data: { type: 'object', additionalProperties: true }, + }, + ['idempotencyKey', 'description', 'prompt'], + ), +); +operation( + 'update_task', + 'task:write', + idempotentWrite, + object( + { + taskId: id, + expectedRevision: positive, + leaseToken: text(4096), + patch: { type: 'object', additionalProperties: true }, + }, + ['taskId', 'expectedRevision', 'patch'], + ), +); +operation( + 'archive_task', + 'task:write', + destructive, + object({ taskId: id, expectedRevision: positive, leaseToken: text(4096) }, [ + 'taskId', + 'expectedRevision', + ]), +); +operation( + 'claim_task', + 'task:write', + write, + object({ taskId: id, leaseSeconds: positive }, ['taskId']), +); +operation( + 'release_task', + 'task:write', + idempotentWrite, + object( + { + taskId: id, + leaseToken: text(4096), + expectedRevision: positive, + idempotencyKey: id, + }, + ['taskId', 'leaseToken', 'expectedRevision', 'idempotencyKey'], + ), +); +operation( + 'complete_task', + 'task:write', + idempotentWrite, + object( + { + taskId: id, + leaseToken: text(4096), + expectedRevision: positive, + idempotencyKey: id, + result: text(262_144), + outcomeKind: { enum: ['success', 'negative', 'inconclusive'] }, + }, + ['taskId', 'leaseToken', 'expectedRevision', 'idempotencyKey', 'result'], + ), +); +operation( + 'task_history', + 'read', + read, + object({ taskId: id, limit: positive }, ['taskId']), +); + +operation( + 'list_memories', + 'read', + read, + object({ cursor: text(4096), limit: positive, text: text(4096) }), +); +operation('get_memory', 'read', read, idInput('slug')); +operation( + 'upsert_memory', + 'memory:write', + idempotentWrite, + object( + { + slug: id, + expectedRevision: positive, + idempotencyKey: id, + description: text(4096), + content: text(131_072), + data: { type: 'object', additionalProperties: true }, + }, + ['slug', 'idempotencyKey', 'description', 'content'], + ), +); +operation( + 'delete_memory', + 'memory:write', + destructive, + object({ slug: id, expectedRevision: positive, idempotencyKey: id }, [ + 'slug', + 'expectedRevision', + 'idempotencyKey', + ]), +); +operation( + 'memory_history', + 'read', + read, + object({ slug: id, limit: positive }, ['slug']), +); + +operation( + 'list_prompts', + 'read', + read, + object({ cursor: text(4096), limit: positive, text: text(4096) }), +); +operation('get_prompt', 'read', read, idInput('promptId')); +operation( + 'create_prompt', + 'prompt:write', + idempotentWrite, + object( + { + idempotencyKey: id, + name: text(256), + promptText: text(131_072), + data: { type: 'object', additionalProperties: true }, + }, + ['idempotencyKey', 'name', 'promptText'], + ), +); +operation( + 'update_prompt', + 'prompt:write', + idempotentWrite, + object( + { + promptId: id, + expectedRevision: positive, + idempotencyKey: id, + patch: { type: 'object', additionalProperties: true }, + }, + ['promptId', 'expectedRevision', 'idempotencyKey', 'patch'], + ), +); +operation( + 'delete_prompt', + 'prompt:write', + destructive, + object({ promptId: id, expectedRevision: positive, idempotencyKey: id }, [ + 'promptId', + 'expectedRevision', + 'idempotencyKey', + ]), +); +operation( + 'prompt_history', + 'read', + read, + object({ promptId: id, limit: positive }, ['promptId']), +); + +assert.equal(definitions.length, 52); +assert.equal(new Set(definitions.map(({ name }) => name)).size, 52); + +const legacyNames = { + add_alias: 'mutate_entity', + add_reference: 'mutate_entity', + add_sitelink: 'mutate_entity', + add_statement: 'mutate_entity', + add_qualifier: 'mutate_entity', + create_entity: 'create_item or create_property', + delete_alias: 'mutate_entity', + delete_reference: 'mutate_entity', + delete_sitelink: 'mutate_entity', + delete_statement: 'mutate_entity', + delete_qualifier: 'mutate_entity', + dry_run_sparql: 'query_sparql with dryRun=true', + get_search_status: 'search_admin with action=health', + query_sparql_update: null, + reset_abandoned_claim: null, + search_status: 'gnolith_status or search_admin with action=health', + set_description: 'mutate_entity', + set_label: 'mutate_entity', + set_rank: 'mutate_entity', +}; + +const catalogBase = { + format: 'gnolith-operation-catalog-v1', + operationSchemaVersion: version, + routePrefix, + operations: definitions, +}; +const canonicalCatalog = canonicalize(catalogBase); +const catalogDigest = createHash('sha256') + .update(canonicalCatalog) + .digest('hex'); +const catalog = { ...catalogBase, catalogDigest }; +const schemaArtifact = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + $id: 'https://gnolith.dev/schemas/workshop/operations-v1.json', + title: 'Gnolith Workshop operation catalog', + ...catalog, + legacyNames, +}; +const statusFixtureBase = { + installationId: 'installation:status-fixture', + baseIri: 'https://status-fixture.invalid/', + principalId: 'principal:status-fixture', + credentialId: 'credential:status-fixture', + activeWorkspaceId: 'workspace:status-fixture', + capabilities: ['read'], + authorizationRevision: 7, + migrationReadiness: { + namespace: '@gnolith/workshop', + version: 11, + ready: true, + }, + compatibility: { diamond: true, taproot: true }, + canonicalReady: true, + authorizationReady: true, + lexicalReady: true, + producers: { + ready: true, + fingerprint: 'c'.repeat(64), + kinds: ['task', 'memory', 'prompt'], + }, + blobReady: true, + versions: { server: '0.5.0', operationSchema: version }, + operationCatalogDigest: catalogDigest, +}; +const diagnostic = (code, backend) => ({ + format: 'gnolith-workshop-semantic-diagnostic-v1', + version: 1, + code, + retryable: true, + backend, + repair: 'retry', +}); +const statusEvidenceFixtures = { + format: 'gnolith-workshop-status-evidence-fixtures-v1', + version: 1, + operationSchemaVersion: version, + fixtures: { + ready: { + ...statusFixtureBase, + semanticState: { + state: 'ready', + configured: true, + revision: 1, + fingerprint: 'd'.repeat(64), + ready: true, + diagnostic: null, + }, + }, + materializationPendingSqlite: { + ...statusFixtureBase, + semanticState: { + state: 'degraded', + configured: true, + revision: 1, + fingerprint: 'd'.repeat(64), + ready: false, + diagnostic: diagnostic('materialization-pending', 'sqlite'), + }, + }, + providerUnavailableQdrant: { + ...statusFixtureBase, + semanticState: { + state: 'degraded', + configured: true, + revision: 2, + fingerprint: 'e'.repeat(64), + ready: false, + diagnostic: diagnostic('provider-unavailable', 'qdrant'), + }, + }, + providerUnavailableAfterRestart: { + ...statusFixtureBase, + semanticState: { + state: 'degraded', + configured: true, + revision: 1, + fingerprint: 'd'.repeat(64), + ready: false, + diagnostic: diagnostic('provider-unavailable', 'sqlite'), + }, + }, + unconfigured: { + ...statusFixtureBase, + semanticState: { + state: 'unconfigured', + configured: false, + revision: null, + fingerprint: null, + ready: false, + diagnostic: null, + }, + }, + }, +}; +const statusEvidenceFixtureJson = await formatWithPrettier( + JSON.stringify(statusEvidenceFixtures), + { parser: 'json', endOfLine: 'lf' }, +); +const operationNames = definitions.map(({ name }) => `'${name}'`).join(' | '); +const ts = `/* Generated by scripts/generate-operation-contract.mjs. Do not edit. */ +export const OPERATION_SCHEMA_VERSION = ${version} as const; +export const OPERATION_ROUTE_PREFIX = ${JSON.stringify(routePrefix)} as const; +export const CATALOG_DIGEST = ${JSON.stringify(catalogDigest)} as const; +export const operationCatalog = ${JSON.stringify(definitions, null, 2)} as const; +export type OperationName = ${operationNames}; +export type OperationInput = + Readonly> & { readonly __operationName?: Name }; +export type OperationOutput = + unknown & { readonly __operationName?: Name }; +export const legacyOperationNames = ${JSON.stringify(legacyNames, null, 2)} as const; +`; + +const outputs = new Map([ + ['src/generated/operations.ts', ts], + [ + 'schemas/gnolith-operations-v1.json', + `${JSON.stringify(schemaArtifact, null, 2)}\n`, + ], + [ + 'schemas/legacy-operation-names-v1.json', + `${JSON.stringify({ format: 'gnolith-legacy-operation-names-v1', names: legacyNames }, null, 2)}\n`, + ], + [ + 'schemas/fixtures/workshop-status-evidence-v1.json', + statusEvidenceFixtureJson, + ], +]); +const check = process.argv.includes('--check'); +for (const [path, contents] of outputs) { + const absolute = resolve(path); + if (check) { + assert.equal( + readFileSync(absolute, 'utf8'), + contents, + `${path} is stale; run npm run generate`, + ); + } else { + mkdirSync(dirname(absolute), { recursive: true }); + writeFileSync(absolute, contents); + } +} +console.error( + `${check ? 'verified' : 'generated'} ${definitions.length} operations (${catalogDigest})`, +); + +function canonicalize(value) { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) + return `[${value.map((entry) => canonicalize(entry)).join(',')}]`; + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`) + .join(',')}}`; +} diff --git a/scripts/inspect-packed-owner.mjs b/scripts/inspect-packed-owner.mjs new file mode 100644 index 0000000..f82b956 --- /dev/null +++ b/scripts/inspect-packed-owner.mjs @@ -0,0 +1,16 @@ +import { pathToFileURL } from 'node:url'; + +const [adapterPath] = process.argv.slice(2); +if (!adapterPath) + throw new Error('usage: node scripts/inspect-packed-owner.mjs '); + +const adapter = await import( + `${pathToFileURL(adapterPath).href}?inspect=${Date.now()}` +); +try { + process.stdout.write( + `${JSON.stringify(await adapter.inspectOwnWorkshopState())}\n`, + ); +} finally { + await adapter.closeFixtureDatabase(); +} diff --git a/scripts/materialize-peer-artifacts.mjs b/scripts/materialize-peer-artifacts.mjs new file mode 100644 index 0000000..cc2f14b --- /dev/null +++ b/scripts/materialize-peer-artifacts.mjs @@ -0,0 +1,158 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { + copyFileSync, + existsSync, + lstatSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { spawnSync } from 'node:child_process'; + +const npmCli = + process.env.npm_execpath ?? + (process.platform === 'win32' + ? join( + dirname(process.execPath), + 'node_modules', + 'npm', + 'bin', + 'npm-cli.js', + ) + : resolve( + dirname(process.execPath), + '..', + 'lib', + 'node_modules', + 'npm', + 'bin', + 'npm-cli.js', + )); +const stagingRoot = mkdtempSync(join(tmpdir(), 'workshop-peer-artifacts-')); +const artifactRoot = resolve(process.env.WORKSHOP_PEER_ARTIFACT_ROOT ?? '..'); +const peerEvidence = JSON.parse( + readFileSync(resolve('docs', 'peer-artifacts-v1.json'), 'utf8'), +); +assert.equal(peerEvidence.format, 'gnolith-workshop-peer-artifacts-v1'); +assert.equal(peerEvidence.version, 1); +const peers = peerEvidence.peers; + +try { + for (const peer of peers) materialize(peer); +} finally { + const resolvedStaging = resolve(stagingRoot); + const resolvedTemp = resolve(tmpdir()); + assert.ok( + resolvedStaging.startsWith(`${resolvedTemp}\\`) || + resolvedStaging.startsWith(`${resolvedTemp}/`), + 'Peer staging directory escaped the operating-system temp directory', + ); + assert.match( + resolvedStaging.split(/[\\/]/u).at(-1) ?? '', + /^workshop-peer-artifacts-/u, + ); + rmSync(resolvedStaging, { recursive: true, force: true }); +} + +function materialize(peer) { + const destination = join( + artifactRoot, + peer.destinationDirectory ?? peer.name, + peer.filename, + ); + const source = join(stagingRoot, peer.name); + if (existsSync(destination)) { + verifyArtifact(destination, peer); + mkdirSync(source, { recursive: true }); + copyFileSync(destination, join(source, peer.filename)); + console.log(`verified existing ${peer.name} artifact`); + return; + } + + run('git', [ + 'clone', + '--quiet', + '--filter=blob:none', + peer.repository, + source, + ]); + run('git', ['-C', source, 'checkout', '--quiet', '--detach', peer.commit]); + const actualCommit = capture('git', ['-C', source, 'rev-parse', 'HEAD']); + assert.equal(actualCommit, peer.commit, `${peer.name} source commit differs`); + run( + process.execPath, + [npmCli, 'ci', '--ignore-scripts=false', '--no-audit', '--no-fund'], + source, + ); + run( + process.execPath, + [npmCli, 'pack', '--pack-destination', source, '--silent'], + source, + ); + + const artifact = join(source, peer.filename); + verifyArtifact(artifact, peer); + mkdirSync(dirname(destination), { recursive: true }); + copyFileSync(artifact, destination); + verifyArtifact(destination, peer); + console.log(`materialized ${peer.name} ${peer.commit} ${peer.sha256}`); +} + +function verifyArtifact(path, peer) { + const stat = lstatSync(path); + assert.ok( + stat.isFile() && !stat.isSymbolicLink(), + `${peer.name} artifact must be regular`, + ); + const bytes = readFileSync(path); + assert.equal(bytes.length, peer.bytes, `${peer.name} artifact size differs`); + assert.equal( + createHash('sha256').update(bytes).digest('hex'), + peer.sha256, + `${peer.name} artifact SHA-256 differs`, + ); + assert.equal( + `sha512-${createHash('sha512').update(bytes).digest('base64')}`, + peer.integrity, + `${peer.name} artifact integrity differs`, + ); +} + +function run(command, args, cwd) { + const result = spawnSync(command, args, { + cwd, + encoding: 'utf8', + stdio: 'inherit', + shell: false, + }); + assert.equal(result.error, undefined, `${command} could not start`); + assert.equal( + result.signal, + null, + `${command} was terminated by ${result.signal}`, + ); + assert.equal(result.status, 0, `${command} exited ${result.status}`); +} + +function capture(command, args) { + const result = spawnSync(command, args, { + encoding: 'utf8', + shell: false, + }); + assert.equal(result.error, undefined, `${command} could not start`); + assert.equal( + result.signal, + null, + `${command} was terminated by ${result.signal}`, + ); + assert.equal( + result.status, + 0, + `${command} exited ${result.status}: ${result.stderr}`, + ); + return result.stdout.trim(); +} diff --git a/scripts/node-sqlite-parity.mjs b/scripts/node-sqlite-parity.mjs index c4bf3fc..e87adff 100644 --- a/scripts/node-sqlite-parity.mjs +++ b/scripts/node-sqlite-parity.mjs @@ -1,361 +1,260 @@ import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { basename, delimiter, dirname, join, resolve } from 'node:path'; +import { join, resolve } from 'node:path'; -const root = mkdtempSync(join(tmpdir(), 'workshop-node-sqlite-')); +const root = mkdtempSync(join(tmpdir(), 'workshop-packed-parity-')); const npmCli = findNpmCli(); -const diamondTarball = process.env.DIAMOND_TARBALL - ? resolve(process.env.DIAMOND_TARBALL) - : packInstalledDiamond(root, npmCli); -const expectedSha256 = process.env.DIAMOND_TARBALL_SHA256; -if (expectedSha256) { - assert.equal( - createHash('sha256').update(readFileSync(diamondTarball)).digest('hex'), - expectedSha256, - 'Diamond tarball SHA-256 does not match the pinned artifact', +try { + const diamondTarball = artifact( + 'DIAMOND_TARBALL', + resolve('..', 'diamond', 'gnolith-diamond-0.5.0.tgz'), + process.env.DIAMOND_TARBALL_SHA256 ?? + '3ce5d33478667dc4ec9a8ae4b4c3e0a94c3bce61e741ba62511ab668d1c20228', + ); + const taprootTarball = artifact( + 'TAPROOT_TARBALL', + resolve('..', 'taproot-artifacts', 'gnolith-taproot-0.5.0.tgz'), + process.env.TAPROOT_TARBALL_SHA256 ?? + '030e0bc4b58873b1ba362cc7ea0a232b769fcd5163ce7679d397c47b8f15b818', + ); + const [packed] = JSON.parse( + execFileSync( + process.execPath, + [npmCli, 'pack', '--json', '--pack-destination', root], + { cwd: process.cwd(), encoding: 'utf8' }, + ), + ); + const workshopTarball = join(root, packed.filename); + writeFileSync( + join(root, 'package.json'), + `${JSON.stringify({ private: true, type: 'module' }, null, 2)}\n`, ); -} - -const [packed] = JSON.parse( execFileSync( process.execPath, - [npmCli, 'pack', '--json', '--pack-destination', root], - { - cwd: process.cwd(), - encoding: 'utf8', - }, - ), -); -const workshopTarball = join(root, packed.filename); -writeFileSync( - join(root, 'package.json'), - `${JSON.stringify({ private: true, type: 'module' }, null, 2)}\n`, -); -execFileSync( - process.execPath, - [ - npmCli, - 'install', - '--ignore-scripts', - '--no-audit', - '--no-fund', - '--no-save', - diamondTarball, - '@gnolith/taproot@0.4.0', - workshopTarball, - ], - { cwd: root, stdio: 'inherit' }, -); - -writeFileSync( - join(root, 'claim-worker.mjs'), - ` -import { NodeSqliteDatabase } from '@gnolith/diamond/node-sqlite'; -import { bootstrapTaprootAuthorization, createTaprootHostWriteCapability } from '@gnolith/taproot'; -import { createHmac } from 'node:crypto'; -import { createWorkshopCore } from '@gnolith/workshop/core'; -import { createWorkshopSearchIntegrationV1 } from '@gnolith/workshop/server'; -const db = new NodeSqliteDatabase(process.argv[2], { busyTimeoutMs: 5_000 }); -const AUTH = { - installationId: 'parity:installation', principalId: process.argv[4], - activeWorkspaceId: 'parity:workspace', workspaceIds: ['parity:workspace'], - capabilities: ['read', 'task-write', 'memory-write', 'knowledge-write', 'admin', 'search:admin'], - authorizationRevision: 1, -}; -const TAPROOT = { baseIri: 'https://parity.workshop.gnolith.test' }; -const hostKey = await crypto.subtle.importKey('raw', new TextEncoder().encode('workshop-parity-host-key-32!!!!'), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); -const hostCapability = createTaprootHostWriteCapability(db, TAPROOT, hostKey); -const search = await createWorkshopSearchIntegrationV1({ db, taproot: TAPROOT, hostCapability, installationId: AUTH.installationId, registrationContext: AUTH }); -const core = createWorkshopCore({ - persistence: db, - authorization: { - getInstallationAuthorizationState: async () => ({ installationId: AUTH.installationId, authorizationRevision: 1, searchGeneration: 1 }), - commitTaskMutation: async (_db, _context, mutation) => mutation.first(), - commitMemoryMutation: async (_db, _context, mutation) => mutation.first(), - commitTaskBackfill: async (_db, _context, _state, mutations) => _db.batch([...mutations]), - commitMemoryBackfill: async (_db, _context, _state, mutations) => _db.batch([...mutations]), - commitCursorSnapshot: async (_db, _context, _state, mutations) => _db.batch([...mutations]), - }, - knowledge: { - authorizedReader: () => ({ - getEntity: async () => null, - searchEntities: async () => ({ items: [], cursor: null }), - }), - health: async () => true, - }, - diamondHealth: async () => true, - cursorCodec: { - currentGeneration: async () => 'parity-generation', - digest: async (purpose, value) => createHmac('sha256', 'workshop-parity-cursor-key').update(purpose).update('\\0').update(value).digest('hex'), - seal: async () => { throw new Error('pagination not exercised'); }, - open: async () => { throw new Error('pagination not exercised'); }, - }, - search, -}); -try { - await core.tasks.claim(process.argv[3], AUTH); - console.log('fulfilled'); -} catch { - console.log('rejected'); + [ + npmCli, + 'install', + '--ignore-scripts', + '--no-audit', + '--no-fund', + '--no-save', + diamondTarball, + taprootTarball, + workshopTarball, + 'typescript@5.9.2', + '@types/node@24', + ], + { cwd: root, stdio: 'inherit' }, + ); + writeFileSync(join(root, 'consumer.mjs'), consumerScript()); + execFileSync(process.execPath, [join(root, 'consumer.mjs')], { + cwd: root, + stdio: 'inherit', + }); + writeFileSync(join(root, 'consumer.ts'), declarationConsumer()); + writeFileSync( + join(root, 'tsconfig.json'), + `${JSON.stringify( + { + compilerOptions: { + strict: true, + noEmit: true, + module: 'NodeNext', + moduleResolution: 'NodeNext', + target: 'ES2023', + types: ['node'], + exactOptionalPropertyTypes: true, + }, + include: ['consumer.ts'], + }, + null, + 2, + )}\n`, + ); + execFileSync( + process.execPath, + [join(root, 'node_modules', 'typescript', 'bin', 'tsc')], + { cwd: root, stdio: 'inherit' }, + ); } finally { - await db.close(); + rmSync(root, { recursive: true, force: true }); } -`, -); - -writeFileSync( - join(root, 'parity.mjs'), - ` -import assert from 'node:assert/strict'; -import { createHmac } from 'node:crypto'; -import { spawn } from 'node:child_process'; -import { join } from 'node:path'; -import { readAppliedMigrations } from '@gnolith/diamond'; -import { NodeSqliteDatabase } from '@gnolith/diamond/node-sqlite'; -import { bootstrapTaprootAuthorization, createTaprootHostWriteCapability, initializeTaproot } from '@gnolith/taproot'; -import { applyWorkshopMigrations, WORKSHOP_SCHEMA_VERSION, workshopMigrations } from '@gnolith/workshop/migrations'; -import { createWorkshopCore } from '@gnolith/workshop/core'; -import { backfillWorkshopAuthorizationBatch, createWorkshopSearchIntegrationV1, HealthService } from '@gnolith/workshop/server'; - -const path = join(${JSON.stringify(root)}, 'workshop.sqlite'); -const TAPROOT = { baseIri: 'https://parity.workshop.gnolith.test' }; -const AUTH = (principalId = 'owner') => ({ - installationId: 'parity:installation', principalId, - activeWorkspaceId: 'parity:workspace', workspaceIds: ['parity:workspace'], - capabilities: ['read', 'task-write', 'memory-write', 'knowledge-write', 'admin', 'search:admin'], - authorizationRevision: 1, -}); -const authorization = { - getInstallationAuthorizationState: async () => - ({ installationId: 'parity:installation', authorizationRevision: 1, searchGeneration: 1 }), - commitTaskMutation: async (_db, _context, mutation) => mutation.first(), - commitMemoryMutation: async (_db, _context, mutation) => mutation.first(), - commitTaskBackfill: async (_db, _context, _state, mutations) => _db.batch([...mutations]), - commitMemoryBackfill: async (_db, _context, _state, mutations) => _db.batch([...mutations]), - commitCursorSnapshot: async (_db, _context, _state, mutations) => _db.batch([...mutations]), -}; -const options = { - authorization, - knowledge: { - authorizedReader: () => ({ - getEntity: async () => null, - searchEntities: async () => ({ items: [], cursor: null }), - }), - health: async () => true, - }, - diamondHealth: async () => true, - cursorCodec: { - currentGeneration: async () => 'parity-generation', - digest: async (purpose, value) => createHmac('sha256', 'workshop-parity-cursor-key').update(purpose).update('\\0').update(value).digest('hex'), - seal: async () => { throw new Error('pagination not exercised'); }, - open: async () => { throw new Error('pagination not exercised'); }, - }, -}; -const searchFor = async (db, adopt = false) => { - const hostKey = await crypto.subtle.importKey('raw', new TextEncoder().encode('workshop-parity-host-key-32!!!!'), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); - const hostCapability = createTaprootHostWriteCapability(db, TAPROOT, hostKey); - const search = await createWorkshopSearchIntegrationV1({ db, taproot: TAPROOT, hostCapability, installationId: AUTH().installationId, registrationContext: AUTH() }); - if (adopt) for (const domain of [search.task, search.memory, search.prompt]) await domain.producer.adoptLegacyPage(AUTH(), { limit: 100 }); - return search; -}; -let first = new NodeSqliteDatabase(path); -await initializeTaproot(first, TAPROOT); -const bootstrapKey = await crypto.subtle.importKey('raw', new TextEncoder().encode('workshop-parity-host-key-32!!!!'), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); -await bootstrapTaprootAuthorization(first, TAPROOT, createTaprootHostWriteCapability(first, TAPROOT, bootstrapKey), AUTH().installationId); -await applyWorkshopMigrations(first); -let search = await searchFor(first, true); -let core = createWorkshopCore({ persistence: first, ...options, search }); -await assert.rejects(first.batch([ - first.prepare("INSERT INTO workshop_memories (slug, description, content, created_at, updated_at, revision) VALUES ('rollback-sentinel', 'Sentinel', 'Must roll back', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1)"), - first.prepare("INSERT INTO workshop_memories (slug, description, content, created_at, updated_at, revision) VALUES ('rollback-failure', NULL, 'Invalid', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1)"), -])); -assert.equal((await first.prepare( - "SELECT COUNT(*) AS count FROM workshop_memories WHERE slug = 'rollback-sentinel'", -).first()).count, 0); -await core.memories.upsert('durable', { description: 'Durable', content: 'Across reopen' }, AUTH()); -const created = await core.tasks.create({ description: 'Portable task', prompt: 'Persist this', memorySlugs: ['durable'] }, AUTH()); -const memory = await core.memories.get('durable', AUTH()); -const changedMemory = await core.memories.upsert('durable', { - description: 'Durable', content: 'Revision two', expectedRevision: memory.revision, -}, AUTH()); -assert.equal(changedMemory.revision, 2); -await assert.rejects(core.memories.upsert('durable', { - description: 'Durable', content: 'Stale', expectedRevision: memory.revision, -}, AUTH())); - -await first.close(); - -first = new NodeSqliteDatabase(path); -search = await searchFor(first); -core = createWorkshopCore({ persistence: first, ...options, search }); -assert.equal((await core.tasks.get(created.id, AUTH())).description, 'Portable task'); -assert.equal((await core.memories.get('durable', AUTH())).content, 'Revision two'); -assert.deepEqual(await applyWorkshopMigrations(first), { - previousVersion: WORKSHOP_SCHEMA_VERSION, - version: WORKSHOP_SCHEMA_VERSION, - applied: [], -}); -const [recorded] = await readAppliedMigrations(first, '@gnolith/workshop'); -await first.prepare( - "UPDATE _gnolith_migrations SET checksum = 'tampered' WHERE namespace = '@gnolith/workshop' AND migration_id = ?", -).bind(recorded.id).run(); -await assert.rejects(applyWorkshopMigrations(first)); -await first.prepare( - "UPDATE _gnolith_migrations SET checksum = ? WHERE namespace = '@gnolith/workshop' AND migration_id = ?", -).bind(recorded.checksum, recorded.id).run(); -const health = await new HealthService({ - db: first, - diamondHealth: core.diamondHealth, - knowledge: core.knowledge, - constructMcp: () => ({}), -}).inspect(); -assert.equal(health.status, 'ok'); -assert.equal(health.checks.persistence, true); -assert.equal(health.checks.d1, true); - -const second = new NodeSqliteDatabase(path); -const competing = createWorkshopCore({ persistence: second, ...options, search: await searchFor(second) }); -const outcomes = await Promise.allSettled([ - core.tasks.claim(created.id, AUTH('agent:first')), - competing.tasks.claim(created.id, AUTH('agent:second')), -]); -assert.equal(outcomes.filter(({ status }) => status === 'fulfilled').length, 1); -assert.equal(outcomes.filter(({ status }) => status === 'rejected').length, 1); -assert.equal((await core.tasks.get(created.id, AUTH())).claimed, true); -await second.close(); -await first.close(); - -first = new NodeSqliteDatabase(path); -search = await searchFor(first); -core = createWorkshopCore({ persistence: first, ...options, search }); -const processTask = await core.tasks.create({ description: 'Process contention', prompt: 'Claim once' }, AUTH()); -await first.close(); -const workerPath = join(${JSON.stringify(root)}, 'claim-worker.mjs'); -const runClaim = (principal) => new Promise((resolve, reject) => { - const child = spawn(process.execPath, [workerPath, path, processTask.id, principal], { - cwd: ${JSON.stringify(root)}, stdio: ['ignore', 'pipe', 'pipe'], - }); - let output = ''; - let error = ''; - child.stdout.on('data', (chunk) => { output += chunk; }); - child.stderr.on('data', (chunk) => { error += chunk; }); - child.on('error', reject); - child.on('close', (code) => code === 0 ? resolve(output.trim()) : reject(new Error(error))); -}); -const processOutcomes = await Promise.all([runClaim('process:a'), runClaim('process:b')]); -assert.deepEqual(processOutcomes.sort(), ['fulfilled', 'rejected'].sort()); -first = new NodeSqliteDatabase(path); -search = await searchFor(first); -core = createWorkshopCore({ persistence: first, ...options, search }); -assert.equal((await core.tasks.get(processTask.id, AUTH())).claimed, true); -await first.close(); - -const legacyPath = join(${JSON.stringify(root)}, 'workshop-v1.sqlite'); -let legacy = new NodeSqliteDatabase(legacyPath); -await initializeTaproot(legacy, TAPROOT); -const legacyBootstrapKey = await crypto.subtle.importKey('raw', new TextEncoder().encode('workshop-parity-host-key-32!!!!'), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); -await bootstrapTaprootAuthorization(legacy, TAPROOT, createTaprootHostWriteCapability(legacy, TAPROOT, legacyBootstrapKey), AUTH().installationId); -const legacyStatements = workshopMigrations[0].sql - .split(/;\\s*(?:\\r?\\n|$)/u) - .map((statement) => statement.trim()) - .filter(Boolean) - .map((statement) => legacy.prepare(statement)); -await legacy.batch(legacyStatements); -await legacy.batch([ - legacy.prepare("INSERT INTO workshop_memories (slug, description, content, created_at, updated_at) VALUES ('legacy-memory', 'Legacy', 'Before migration', '2026-07-20 12:00:00', '2026-07-20 12:00:00')"), - legacy.prepare("INSERT INTO workshop_tasks (id, description, prompt, context_queries, memory_slugs, claimed, created_at, updated_at) VALUES ('legacy-update', 'Legacy update', 'Before migration', '[]', json_array('legacy-memory'), 0, '2026-07-20 12:00:00', '2026-07-20 12:00:00')"), - legacy.prepare("INSERT INTO workshop_tasks (id, description, prompt, context_queries, memory_slugs, claimed, created_at, updated_at) VALUES ('legacy-archive', 'Legacy archive', 'Before migration', '[]', '[]', 0, '2026-07-20 12:00:00', '2026-07-20 12:00:00')"), -]); -await applyWorkshopMigrations(legacy); -const legacyRecords = await readAppliedMigrations(legacy, '@gnolith/workshop'); -assert.deepEqual(legacyRecords.map(({ adopted }) => adopted), [ - true, - false, - false, - false, - false, - false, -]); -const legacySearch = await searchFor(legacy); -const legacyCore = createWorkshopCore({ persistence: legacy, ...options, search: legacySearch }); -await assert.rejects(legacyCore.memories.get('legacy-memory', AUTH())); -await assert.rejects(legacyCore.tasks.get('legacy-update', AUTH())); -await backfillWorkshopAuthorizationBatch(legacy, authorization, AUTH(), { domain: 'memory' }); -await backfillWorkshopAuthorizationBatch(legacy, authorization, AUTH(), { domain: 'task' }); -for (const domain of [legacySearch.task, legacySearch.memory, legacySearch.prompt]) await domain.producer.adoptLegacyPage(AUTH(), { limit: 100 }); -const legacyMemory = await legacyCore.memories.get('legacy-memory', AUTH()); -assert.equal((await legacyCore.memories.upsert('legacy-memory', { - description: 'Legacy', content: 'Migrated', expectedUpdatedAt: legacyMemory.updatedAt, -}, AUTH())).revision, 2); -const legacyTask = await legacyCore.tasks.get('legacy-update', AUTH()); -assert.equal((await legacyCore.tasks.update('legacy-update', { - expectedUpdatedAt: legacyTask.updatedAt, prompt: 'Migrated update', -}, AUTH())).revision, 2); -const legacyArchive = await legacyCore.tasks.get('legacy-archive', AUTH()); -assert.equal((await legacyCore.tasks.archive('legacy-archive', { - expectedUpdatedAt: legacyArchive.updatedAt, -}, AUTH())).archivedAt !== undefined, true); -await legacy.close(); -console.log('Workshop packed-artifact node:sqlite reopen, migrations, revisions, health, connection and process contention parity passed'); -`, -); -execFileSync(process.execPath, [join(root, 'parity.mjs')], { - cwd: root, - stdio: 'inherit', -}); -console.log( - `verified ${basename(workshopTarball)} with ${basename(diamondTarball)}`, -); -function fail(message) { - throw new Error(message); +function artifact(environmentName, fallback, expectedSha256) { + const path = resolve(process.env[environmentName] ?? fallback); + assert.equal( + sha256(readFileSync(path)), + expectedSha256, + `${environmentName} SHA-256 does not match the exact candidate`, + ); + return path; } -function packInstalledDiamond(destination, npmCli) { - const installed = resolve('node_modules/@gnolith/diamond'); - if (!existsSync(installed)) - fail('DIAMOND_TARBALL is required when @gnolith/diamond is not installed'); - const [packed] = JSON.parse( - execFileSync( - process.execPath, - [ - npmCli, - 'pack', - '--ignore-scripts', - '--json', - '--pack-destination', - destination, - ], - { cwd: installed, encoding: 'utf8' }, - ), - ); - return join(destination, packed.filename); +function sha256(bytes) { + return createHash('sha256').update(bytes).digest('hex'); } function findNpmCli() { const candidates = [ process.env.npm_execpath, - join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'), join( - dirname(process.execPath), + process.execPath, + '..', '..', - 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js', ), - ...String(process.env.PATH ?? '') - .split(delimiter) - .filter(Boolean) - .map((entry) => join(entry, 'node_modules', 'npm', 'bin', 'npm-cli.js')), - ]; - const npmCli = candidates.find( - (candidate) => candidate && existsSync(candidate), + ].filter(Boolean); + const cli = candidates.find((candidate) => { + try { + return readFileSync(candidate).length > 0; + } catch { + return false; + } + }); + assert.ok(cli, 'npm CLI was not found'); + return cli; +} + +function consumerScript() { + return String.raw` +import assert from 'node:assert/strict'; +import { createHash, randomBytes } from 'node:crypto'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { NodeSqliteDatabase } from '@gnolith/diamond/node-sqlite'; +import { + commitAuthorizationBootstrapV1, + createTaprootHostWriteCapability, + initializeTaproot, + prepareAuthorizationBootstrapV1, + registerAuthorizationBootstrapOwnerV1, +} from '@gnolith/taproot'; +import { workshopPackage } from '@gnolith/workshop'; +import { + bootstrapWorkshopV1, + createWorkshopBootstrapOwnerAdapterV1, +} from '@gnolith/workshop/bootstrap'; +import { + createLocalFilesystemResourceBlobPortsV1, +} from '@gnolith/workshop/backup'; +import { + applyWorkshopMigrations, + WORKSHOP_SCHEMA_VERSION, +} from '@gnolith/workshop/migrations'; + +assert.equal(workshopPackage.version, '0.5.0'); +assert.equal(workshopPackage.schemaVersion, 11); +assert.equal(WORKSHOP_SCHEMA_VERSION, 11); +const temporary = await mkdtemp(join(tmpdir(), 'workshop-consumer-')); +const path = join(temporary, 'shared.sqlite'); +const baseIri = 'https://packed-parity.example'; +const installationId = 'installation:packed-parity'; +let db = new NodeSqliteDatabase(path); +try { + await initializeTaproot(db, { baseIri }); + assert.deepEqual(await applyWorkshopMigrations(db), { + previousVersion: 0, + version: 11, + applied: [ + '0001_workshop', '0002_revisions', '0003_resumable_onboarding', + '0004_authorization', '0005_cursor_snapshots', + '0006_canonical_search_domains', '0007_task_leases', + '0008_bootstrap_credentials', + '0009_operation_receipts', + '0010_durable_mutation_receipts', + '0011_administration_control_plane', + ], + }); + const key = await crypto.subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'], + ); + const authority = createTaprootHostWriteCapability(db, { baseIri }, key); + const ownerAdapter = createWorkshopBootstrapOwnerAdapterV1(db); + const issuer = await registerAuthorizationBootstrapOwnerV1( + db, { baseIri }, authority, + { installationId, ownerRegistrationId: 'workshop', adapter: ownerAdapter.adapter }, + ); + const raw = randomBytes(32); + const credentialFile = new TextEncoder().encode(raw.toString('base64url')); + const receipt = await bootstrapWorkshopV1( + { + version: 1, installationId, baseIri, + principalId: 'principal:owner', workspaceId: 'workspace:default', + credential: { kind: 'file', value: '/protected' }, + idempotencyKey: 'packed-owner-v1', profile: 'local-bearer-v1', + }, + { + prepareAuthorizationBootstrapV1: (input) => + prepareAuthorizationBootstrapV1(db, { baseIri }, authority, input), + ownerParticipantIssuer: issuer, + ownerAdapter, + commitAuthorizationBootstrapV1, + readProtectedCredential: async () => credentialFile.slice(), + workshopSchemaVersion: 11, + }, + ); + assert.equal(receipt.installationId, installationId); + const blobRoot = join(temporary, 'blobs'); + const blobs = await createLocalFilesystemResourceBlobPortsV1({ root: blobRoot }); + const bytes = new TextEncoder().encode('packed payload'); + const descriptor = { + referenceId: 'blob:packed', resourceId: 'resource:packed', revision: 1, + byteLength: bytes.byteLength, + sha256: createHash('sha256').update(bytes).digest('hex'), + mediaType: 'text/plain', + }; + const stage = await blobs.restore.stage( + descriptor, + new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } }), ); - if (!npmCli) throw new Error('Unable to locate npm-cli.js'); - return npmCli; + await blobs.restore.verify(stage); + await blobs.restore.commit([stage]); + assert.deepEqual((await blobs.backup.enumerate(null, 10)).items, [descriptor]); + await db.close(); + db = new NodeSqliteDatabase(path); + assert.deepEqual(await applyWorkshopMigrations(db), { + previousVersion: 11, version: 11, applied: [], + }); +} finally { + await db.close().catch(() => {}); + await rm(temporary, { recursive: true, force: true }); +} +`; +} + +function declarationConsumer() { + return ` +import type { + ResourceBlobBackupPort, + ResourceBlobRestorePort, +} from '@gnolith/taproot/backup'; +import { + createLocalFilesystemResourceBlobPortsV1, + type LocalFilesystemResourceBlobPortsV1, +} from '@gnolith/workshop/backup'; +import { + createWorkshopCanonicalOwnerAdapterV1, + type WorkshopSearchDomainV1, +} from '@gnolith/workshop/server'; + +async function consume(): Promise { + const ports: LocalFilesystemResourceBlobPortsV1 = + await createLocalFilesystemResourceBlobPortsV1({ root: '/absolute/blobs' }); + const backup: ResourceBlobBackupPort = ports.backup; + const restore: ResourceBlobRestorePort = ports.restore; + void backup; + void restore; + void createWorkshopCanonicalOwnerAdapterV1; + const domain: WorkshopSearchDomainV1 | null = null; + void domain; +} +void consume; +`; } diff --git a/scripts/packed-process-smoke.mjs b/scripts/packed-process-smoke.mjs new file mode 100644 index 0000000..04d45fa --- /dev/null +++ b/scripts/packed-process-smoke.mjs @@ -0,0 +1,1836 @@ +/* global fetch */ +import assert from 'node:assert/strict'; +import { Buffer } from 'node:buffer'; +import { execFile, execFileSync, spawn } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; +import { rm, writeFile } from 'node:fs/promises'; +import { createServer as createHttpServer } from 'node:http'; +import { connect, createServer } from 'node:net'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { setTimeout } from 'node:timers'; +import { promisify } from 'node:util'; + +const root = resolve(import.meta.dirname, '..'); +const execFileAsync = promisify(execFile); +const packed = JSON.parse( + execFileSync('npm', ['pack', '--json'], { + cwd: root, + encoding: 'utf8', + shell: process.platform === 'win32', + }), +); +const tarball = resolve(root, packed[0].filename); +const fixture = mkdtempSync(join(tmpdir(), 'gnolith-workshop-process-')); +let child; +let semanticMock; +let semanticMockPoints; +try { + writeFileSync( + join(fixture, 'package.json'), + JSON.stringify({ + private: true, + type: 'module', + dependencies: { + '@gnolith/workshop': `file:${tarball}`, + '@gnolith/diamond': `file:${resolve(root, '..', 'diamond', 'gnolith-diamond-0.5.0.tgz')}`, + '@gnolith/taproot': `file:${resolve(root, '..', 'taproot-artifacts', 'gnolith-taproot-0.5.0.tgz')}`, + }, + }), + ); + execFileSync('npm', ['install', '--ignore-scripts'], { + cwd: fixture, + stdio: 'ignore', + shell: process.platform === 'win32', + }); + const semanticMockRuntime = await startSemanticMock(); + semanticMock = semanticMockRuntime.server; + semanticMockPoints = semanticMockRuntime.points; + const semanticMockOrigin = semanticMockRuntime.origin; + const token = Buffer.from( + Uint8Array.from({ length: 32 }, (_, index) => index), + ).toString('base64url'); + const port = await availablePort(); + await writeFile( + join(fixture, 'adapter.mjs'), + ` +import { Buffer } from 'node:buffer'; +import { lstat, readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { + createWorkshopLocalExecutableAdapterFromPrimitivesV1, +} from '@gnolith/workshop/local-adapter'; +import { verifyWaystoneAssetMount } from '@gnolith/workshop/server'; +import { + applyWorkshopMigrations, +} from '@gnolith/workshop/migrations'; +import { + createWorkshopBackupApi, + createLocalFilesystemResourceBlobPortsV1, +} from '@gnolith/workshop/backup'; +import { + createMigrationAssemblyAuthorityV1, + migrateDiamondStore, +} from '@gnolith/diamond'; +import { NodeSqliteDatabase } from '@gnolith/diamond/node-sqlite'; +import { + createAuthorizationCursorCodec, + createTaprootHostWriteCapability, + initializeTaproot, +} from '@gnolith/taproot'; + +assertFactory(createWorkshopLocalExecutableAdapterFromPrimitivesV1); +function assertFactory(value) { + if (typeof value !== 'function') throw new Error('packed local adapter factory is unavailable'); +} + +const token = Buffer.from( + Uint8Array.from({ length: 32 }, (_, index) => index), +).toString('base64url'); +const installationId = 'installation:packed'; +const baseIri = 'https://packed.invalid/'; +const db = new NodeSqliteDatabase( + fileURLToPath(new URL('./shared.sqlite', import.meta.url)), +); +await migrateDiamondStore(db); +await initializeTaproot(db, { baseIri }); +await applyWorkshopMigrations(db); +const hostKey = await crypto.subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'], +); +const hostAuthority = createTaprootHostWriteCapability( + db, { baseIri }, hostKey, +); +const taprootCursorKey = await crypto.subtle.generateKey( + { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'], +); +const workshopCursorKey = await crypto.subtle.generateKey( + { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'], +); +const workshopDigestKey = await crypto.subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'], +); +const blobs = await createLocalFilesystemResourceBlobPortsV1({ + root: fileURLToPath(new URL('./blobs', import.meta.url)), +}); +const adapter = await createWorkshopLocalExecutableAdapterFromPrimitivesV1({ + kind: 'workshop-local-assembly-primitives-v1', + version: '0.5.0', + db, + installationId, + registrationFingerprint: 'd'.repeat(64), + taproot: { + options: { baseIri }, + hostAuthority, + authorizationCursorCodec: createAuthorizationCursorCodec(taprootCursorKey), + registrationPrincipal: { + principalId: 'principal:packed', + activeWorkspaceId: 'workspace:packed', + }, + payloadStore: blobs.payloadStore, + }, + cursor: { + generation: 'packed-generation-v1', + encryptionKey: workshopCursorKey, + digestKey: workshopDigestKey, + }, + migrationAssemblyAuthority: + createMigrationAssemblyAuthorityV1(db, installationId), + semantic: { + resolveCredential(selector) { + if (selector === 'provider:packed') return 'packed-openai-secret'; + if (selector === 'qdrant:packed') return 'packed-qdrant-secret'; + return undefined; + }, + }, + assets: async (config) => { + if (!config.assetMount) return undefined; + const root = fileURLToPath(new URL('./assets/', import.meta.url)); + const manifest = JSON.parse( + await readFile(new URL('./asset-manifest.json', import.meta.url), 'utf8'), + ); + return verifyWaystoneAssetMount({ + prefix: config.assetMount.prefix, + manifest, + source: { + async read(path) { + const target = new URL(path, new URL('./assets/', import.meta.url)); + const metadata = await lstat(target).catch(() => null); + if (!metadata) return null; + return { bytes: await readFile(target), symlink: metadata.isSymbolicLink() }; + }, + }, + }); + }, + dependencies: [{ close: () => db.close() }], +}); +export async function inspectOwnWorkshopState() { + const state = await adapter.ownerHandles.owner.exportOwnedState({ + installationId, + signal: new AbortController().signal, + }); + const roundTrippedState = JSON.parse(JSON.stringify(state)); + const ownerInspection = await adapter.ownerHandles.owner.inspectOwnedState({ + state: roundTrippedState, + signal: new AbortController().signal, + }); + const backup = createWorkshopBackupApi({ + owner: adapter.ownerHandles.owner, + ledger: adapter.ownerHandles.ledger, + schemaVersion: 11, + }); + const document = await backup.exportBackup(installationId); + const fullInspection = await backup.inspectBackup( + JSON.parse(JSON.stringify(document)), + ); + return { + ownerInspection, + fullInspection, + taskIdentities: state.tables.workshop_tasks.map((row) => ({ + id: row.id, + idempotencyKey: row.idempotency_key, + })), + }; +} +export async function closeFixtureDatabase() { + await db.close(); +} +export async function createWorkshopExecutable(config, protectedReader) { + return adapter.createWorkshopExecutable(config, protectedReader); +} +export async function createWorkshopBootstrapDependencies( + config, protectedReader, +) { + return adapter.createWorkshopBootstrapDependencies(config, protectedReader); +} +export async function createWorkshopSemanticConfigurationDependencies( + config, protectedReader, +) { + return adapter.createWorkshopSemanticConfigurationDependencies( + config, protectedReader, + ); +} +`, + ); + const credentialPath = join(fixture, 'credential.txt'); + writeFileSync(credentialPath, token); + const assetBytes = Buffer.from( + 'Packed Waystone', + ); + mkdirSync(join(fixture, 'assets')); + writeFileSync(join(fixture, 'assets', 'index.html'), assetBytes); + const unsignedAssetManifest = { + format: 'waystone-assets-v1', + files: [ + { + path: 'index.html', + sha256: createHash('sha256').update(assetBytes).digest('hex'), + bytes: assetBytes.byteLength, + mediaType: 'text/html; charset=utf-8', + immutable: true, + }, + ], + entrypoint: 'index.html', + }; + writeFileSync( + join(fixture, 'asset-manifest.json'), + JSON.stringify({ + ...unsignedAssetManifest, + manifestSha256: createHash('sha256') + .update(canonicalize(unsignedAssetManifest)) + .digest('hex'), + }), + ); + await writeFile( + join(fixture, 'config.json'), + JSON.stringify({ + format: 'gnolith-workshop-config-v1', + listener: { host: '127.0.0.1', port, profile: 'test' }, + authentication: { + profile: 'local-bearer-v1', + credentialStore: { kind: 'file', value: '/unused' }, + }, + adapterModule: './adapter.mjs', + assetMount: { + prefix: '/app', + manifest: { kind: 'file', value: './asset-manifest.json' }, + root: { kind: 'file', value: './assets' }, + }, + }), + ); + const bootstrap = JSON.parse( + execFileSync( + process.execPath, + [ + join(fixture, 'node_modules', '@gnolith', 'workshop', 'dist', 'cli.js'), + 'bootstrap-v1', + '--config', + join(fixture, 'config.json'), + '--installation-id', + 'installation:packed', + '--base-iri', + 'https://packed.invalid/', + '--principal', + 'principal:packed', + '--workspace', + 'workspace:packed', + '--credential-file', + credentialPath, + '--idempotency-key', + 'packed-owner-v1', + ], + { cwd: fixture, encoding: 'utf8' }, + ), + ); + assert.equal(bootstrap.installationId, 'installation:packed'); + const semanticInputPath = join(fixture, 'semantic-config.json'); + await writeFile( + semanticInputPath, + JSON.stringify({ + version: 1, + expectedRevision: 0, + idempotencyKey: 'packed-semantic-v1', + configuration: { + version: 1, + id: 'packed-openai-sqlite-v1', + name: 'Packed OpenAI SQLite', + provider: { + kind: 'openai-compatible', + endpoint: `${semanticMockOrigin}/v1/embeddings`, + model: 'text-embedding-packed', + dimensions: 4, + metric: 'cosine', + credentialSelector: 'provider:packed', + allowPrivateEndpoint: true, + redirectPolicy: 'error', + }, + vector: { kind: 'sqlite' }, + }, + }), + ); + const semanticReceipt = JSON.parse( + ( + await execFileAsync( + process.execPath, + [ + join( + fixture, + 'node_modules', + '@gnolith', + 'workshop', + 'dist', + 'cli.js', + ), + 'configure-semantics-v1', + '--config', + join(fixture, 'config.json'), + '--input', + semanticInputPath, + ], + { cwd: fixture, encoding: 'utf8' }, + ) + ).stdout, + ); + assert.equal(semanticReceipt.configurationId, 'packed-openai-sqlite-v1'); + assert.equal(semanticReceipt.revision, 1); + assert.equal( + semanticReceipt.state, + 'building', + JSON.stringify(semanticReceipt), + ); + assert.match(semanticReceipt.fingerprint, /^[a-f0-9]{64}$/u); + child = spawn( + process.execPath, + [ + join(fixture, 'node_modules', '@gnolith', 'workshop', 'dist', 'cli.js'), + 'serve', + '--config', + join(fixture, 'config.json'), + ], + { cwd: fixture, stdio: 'ignore' }, + ); + const origin = `http://127.0.0.1:${port}`; + await waitForLive(origin, child); + const delegatedTokenBytes = Uint8Array.from( + { length: 32 }, + (_, index) => 255 - index, + ); + const delegatedToken = Buffer.from(delegatedTokenBytes).toString('base64url'); + const delegatedDigest = createHash('sha256') + .update(Buffer.from('gnolith-local-bearer-v1\0', 'utf8')) + .update(delegatedTokenBytes) + .digest('hex'); + const initialStatus = await successfulToolCall( + origin, + token, + 100, + 'gnolith_status', + {}, + ); + assert.deepEqual(initialStatus.semanticState.diagnostic, { + format: 'gnolith-workshop-semantic-diagnostic-v1', + version: 1, + code: 'materialization-pending', + retryable: true, + backend: 'sqlite', + repair: 'retry', + }); + let policyRevision = initialStatus.authorizationRevision; + const authorizationMutation = async (id, argumentsValue) => { + const result = await successfulToolCall( + origin, + token, + id, + 'authorization_admin', + { + version: 1, + expectedPolicyRevision: policyRevision, + ...argumentsValue, + }, + ); + policyRevision = result.policyRevision; + return result; + }; + await authorizationMutation(101, { + action: 'principal-create', + idempotencyKey: 'packed-delegated-principal-v1', + principal: { principalId: 'principal:delegated', enabled: true }, + }); + await authorizationMutation(102, { + action: 'workspace-membership-add', + idempotencyKey: 'packed-delegated-membership-v1', + principalId: 'principal:delegated', + workspaceId: 'workspace:packed', + }); + for (const [index, capability] of [ + 'read', + 'admin', + 'search:admin', + ].entries()) { + await authorizationMutation(103 + index, { + action: 'workspace-grant-add', + idempotencyKey: `packed-delegated-grant-${capability}-v1`, + principalId: 'principal:delegated', + workspaceId: 'workspace:packed', + capability, + }); + } + const registered = await authorizationMutation(105, { + action: 'credential-register', + idempotencyKey: 'packed-delegated-credential-v1', + credential: { + credentialId: 'credential:delegated', + tokenDigest: delegatedDigest, + principalId: 'principal:delegated', + workspaceId: 'workspace:packed', + }, + }); + assert.doesNotMatch( + JSON.stringify(registered), + new RegExp(delegatedToken, 'u'), + ); + assert.doesNotMatch( + JSON.stringify(registered), + new RegExp(delegatedDigest, 'u'), + ); + const delegatedStatus = await successfulToolCall( + origin, + delegatedToken, + 106, + 'gnolith_status', + {}, + ); + assert.equal(delegatedStatus.principalId, 'principal:delegated'); + await successfulToolCall(origin, delegatedToken, 107, 'authorization_admin', { + version: 1, + action: 'status', + }); + const delegatedGrant = await successfulToolCall( + origin, + delegatedToken, + 108, + 'authorization_admin', + { + version: 1, + action: 'workspace-grant-add', + expectedPolicyRevision: policyRevision, + idempotencyKey: 'packed-delegated-self-policy-v1', + principalId: 'principal:delegated', + workspaceId: 'workspace:packed', + capability: 'knowledge:policy', + }, + ); + policyRevision = delegatedGrant.policyRevision; + await authorizationMutation(109, { + action: 'credential-revoke', + idempotencyKey: 'packed-delegated-credential-revoke-v1', + credentialId: 'credential:delegated', + }); + assert.equal( + await mcpHttpStatus(origin, delegatedToken, 110, 'gnolith_status', {}), + 401, + ); + await authorizationMutation(111, { + action: 'credential-register', + idempotencyKey: 'packed-delegated-credential-reregister-v1', + credential: { + credentialId: 'credential:delegated', + tokenDigest: delegatedDigest, + principalId: 'principal:delegated', + workspaceId: 'workspace:packed', + }, + }); + await successfulToolCall(origin, delegatedToken, 112, 'gnolith_status', {}); + await authorizationMutation(113, { + action: 'principal-disable', + idempotencyKey: 'packed-delegated-principal-disable-v1', + principalId: 'principal:delegated', + }); + assert.equal( + await mcpHttpStatus(origin, delegatedToken, 114, 'gnolith_status', {}), + 401, + ); + await successfulToolCall(origin, token, 115, 'authorization_admin', { + version: 1, + action: 'status', + }); + const created = await mcp(origin, token, 4, 'tools/call', { + name: 'create_item', + arguments: { + idempotencyKey: 'packed-restart-item-v1', + authoredText: 'Create an item immediately before a clean restart.', + data: { + id: 'Q900', + labels: { + en: { language: 'en', value: 'Packed restart item' }, + }, + }, + }, + }); + assert.equal(created.result?.isError, false, JSON.stringify(created)); + const revisionTwo = await successfulToolCall( + origin, + token, + 116, + 'mutate_entity', + { + entityId: 'Q900', + expectedRevision: 1, + idempotencyKey: 'packed-changed-label-2', + mutations: [ + { + kind: 'label', + authoredText: 'Set packed label revision two.', + value: { + type: 'set-label', + language: 'en', + value: 'Packed restart item revision two', + }, + }, + ], + }, + ); + assert.equal(revisionTwo.changed, true); + assert.equal(revisionTwo.newRevision, 2); + const beforeIdenticalStatus = await successfulToolCall( + origin, + token, + 117, + 'gnolith_status', + {}, + ); + const beforeIdenticalHealth = await successfulToolCall( + origin, + token, + 118, + 'search_admin', + { action: 'health' }, + ); + const identicalCalls = await Promise.all( + ['a', 'b'].map((suffix, index) => + successfulToolCall(origin, token, 119 + index, 'mutate_entity', { + entityId: 'Q900', + expectedRevision: 2, + idempotencyKey: `packed-identical-label-${suffix}`, + mutations: [ + { + kind: 'label', + authoredText: 'Preserve the packed restart item label.', + value: { + type: 'set-label', + language: 'en', + value: 'Packed restart item revision two', + }, + }, + ], + }), + ), + ); + for (const identical of identicalCalls) { + assert.equal(identical.changed, false); + assert.equal(identical.newRevision, 2); + assert.deepEqual(identical.quadPatch, { deleted: 0, inserted: 0 }); + } + const afterIdenticalStatus = await successfulToolCall( + origin, + token, + 121, + 'gnolith_status', + {}, + ); + const afterIdenticalHealth = await successfulToolCall( + origin, + token, + 122, + 'search_admin', + { action: 'health' }, + ); + assert.equal( + afterIdenticalStatus.authorizationRevision, + beforeIdenticalStatus.authorizationRevision, + ); + assert.equal( + afterIdenticalHealth.materialization.sourceHighWatermark, + beforeIdenticalHealth.materialization.sourceHighWatermark, + ); + assert.equal( + afterIdenticalHealth.materialization.activeAppliedWatermark, + beforeIdenticalHealth.materialization.activeAppliedWatermark, + ); + assert.equal( + afterIdenticalHealth.materialization.activeCorpusGeneration, + beforeIdenticalHealth.materialization.activeCorpusGeneration, + ); + assert.equal( + afterIdenticalHealth.materialization.cursorGeneration, + beforeIdenticalHealth.materialization.cursorGeneration, + ); + const revisionThree = await successfulToolCall( + origin, + token, + 123, + 'mutate_entity', + { + entityId: 'Q900', + expectedRevision: 2, + idempotencyKey: 'packed-changed-label-3', + mutations: [ + { + kind: 'label', + authoredText: 'Set packed label revision three.', + value: { + type: 'set-label', + language: 'en', + value: 'Packed restart item revision three', + }, + }, + ], + }, + ); + assert.equal(revisionThree.changed, true); + assert.equal(revisionThree.newRevision, 3); + policyRevision = ( + await successfulToolCall(origin, token, 124, 'gnolith_status', {}) + ).authorizationRevision; + const beforeVisibilityAdministration = await successfulToolCall( + origin, + token, + 125, + 'authorization_admin', + { version: 1, action: 'status' }, + ); + const visibilityArguments = { + version: 1, + action: 'visibility-policy-set', + expectedPolicyRevision: policyRevision, + idempotencyKey: 'packed-q1-visibility-v1', + target: { kind: 'entity', id: 'Q900' }, + expectedTargetRevision: 3, + visibility: { + version: 1, + clauses: [[{ kind: 'workspace', workspaceId: 'workspace:packed' }]], + }, + }; + const visibility = await successfulToolCall( + origin, + token, + 126, + 'authorization_admin', + visibilityArguments, + ); + policyRevision = visibility.policyRevision; + const visibilityReplay = await successfulToolCall( + origin, + token, + 127, + 'authorization_admin', + visibilityArguments, + ); + assert.equal(visibilityReplay.status, 'replayed'); + assert.equal(visibilityReplay.auditId, visibility.auditId); + const afterVisibilityAdministration = await successfulToolCall( + origin, + token, + 128, + 'authorization_admin', + { version: 1, action: 'status' }, + ); + assert.equal( + afterVisibilityAdministration.value.audits, + beforeVisibilityAdministration.value.audits + 1, + ); + const effectiveVisibility = await successfulToolCall( + origin, + token, + 129, + 'authorization_admin', + { + version: 1, + action: 'effective-policy', + target: { kind: 'entity', id: 'Q900' }, + }, + ); + assert.equal(effectiveVisibility.value.sourceRevision, 4); + assert.equal( + effectiveVisibility.value.sourcePolicyRevision, + visibility.policyRevision, + ); + assert.deepEqual( + effectiveVisibility.value.visibility, + visibilityArguments.visibility, + ); + const visibilityConflict = await mcp(origin, token, 130, 'tools/call', { + name: 'authorization_admin', + arguments: { + ...visibilityArguments, + visibility: { version: 1, clauses: [] }, + }, + }); + assert.ok( + visibilityConflict.error || visibilityConflict.result?.isError === true, + JSON.stringify(visibilityConflict), + ); + const outsiderBytes = Buffer.from( + Uint8Array.from({ length: 32 }, (_, index) => index + 120), + ); + const outsiderToken = outsiderBytes.toString('base64url'); + const outsiderDigest = createHash('sha256') + .update(Buffer.from('gnolith-local-bearer-v1\0', 'utf8')) + .update(outsiderBytes) + .digest('hex'); + for (const [index, argumentsValue] of [ + { + action: 'principal-create', + idempotencyKey: 'packed-outsider-principal-v1', + principal: { principalId: 'principal:outsider', enabled: true }, + }, + { + action: 'workspace-membership-add', + idempotencyKey: 'packed-outsider-membership-v1', + principalId: 'principal:outsider', + workspaceId: 'workspace:outsider', + }, + { + action: 'workspace-grant-add', + idempotencyKey: 'packed-outsider-read-v1', + principalId: 'principal:outsider', + workspaceId: 'workspace:outsider', + capability: 'read', + }, + { + action: 'credential-register', + idempotencyKey: 'packed-outsider-credential-v1', + credential: { + credentialId: 'credential:outsider', + tokenDigest: outsiderDigest, + principalId: 'principal:outsider', + workspaceId: 'workspace:outsider', + }, + }, + ].entries()) { + const administered = await successfulToolCall( + origin, + token, + 131 + index, + 'authorization_admin', + { + version: 1, + expectedPolicyRevision: policyRevision, + ...argumentsValue, + }, + ); + policyRevision = administered.policyRevision; + } + const orVisibilityEntity = await successfulToolCall( + origin, + token, + 150, + 'create_item', + { + idempotencyKey: 'packed-owner-reader-or-item-v1', + authoredText: + 'Create the owner and reader OR visibility restart fixture.', + data: { + id: 'Q2', + labels: { + en: { + language: 'en', + value: 'Orchard heliotrope visibility proof', + }, + }, + }, + }, + ); + assert.equal(orVisibilityEntity.newRevision, 1); + policyRevision = ( + await successfulToolCall(origin, token, 151, 'gnolith_status', {}) + ).authorizationRevision; + const beforeRejectedOr = await successfulToolCall( + origin, + token, + 152, + 'authorization_admin', + { version: 1, action: 'status' }, + ); + const rejectedAndVisibility = await mcp(origin, token, 153, 'tools/call', { + name: 'authorization_admin', + arguments: { + version: 1, + action: 'visibility-policy-set', + expectedPolicyRevision: policyRevision, + idempotencyKey: 'packed-owner-reader-and-rejected-v1', + target: { kind: 'entity', id: 'Q2' }, + expectedTargetRevision: 1, + visibility: { + version: 1, + clauses: [ + [{ kind: 'principal', principalId: 'principal:packed' }], + [{ kind: 'principal', principalId: 'principal:outsider' }], + ], + }, + }, + }); + assert.equal( + toolErrorCode(rejectedAndVisibility), + 'validation_failed', + JSON.stringify(rejectedAndVisibility), + ); + assert.match(JSON.stringify(rejectedAndVisibility), /unsatisfiable/u); + const afterRejectedOr = await successfulToolCall( + origin, + token, + 154, + 'authorization_admin', + { version: 1, action: 'status' }, + ); + assert.equal( + afterRejectedOr.value.audits, + beforeRejectedOr.value.audits, + JSON.stringify(afterRejectedOr), + ); + const acceptedOrVisibility = await successfulToolCall( + origin, + token, + 155, + 'authorization_admin', + { + version: 1, + action: 'visibility-policy-set', + expectedPolicyRevision: policyRevision, + idempotencyKey: 'packed-owner-reader-or-v1', + target: { kind: 'entity', id: 'Q2' }, + expectedTargetRevision: 1, + visibility: { + version: 1, + clauses: [ + [ + { kind: 'principal', principalId: 'principal:packed' }, + { kind: 'principal', principalId: 'principal:outsider' }, + ], + ], + }, + }, + ); + policyRevision = acceptedOrVisibility.policyRevision; + const acceptedOrStatus = await successfulToolCall( + origin, + token, + 156, + 'authorization_admin', + { version: 1, action: 'status' }, + ); + assert.equal( + acceptedOrStatus.value.audits, + beforeRejectedOr.value.audits + 1, + ); + const acceptedOrPolicy = await successfulToolCall( + origin, + token, + 157, + 'authorization_admin', + { + version: 1, + action: 'effective-policy', + target: { kind: 'entity', id: 'Q2' }, + }, + ); + assert.equal(acceptedOrPolicy.value.sourceRevision, 2); + assert.deepEqual(acceptedOrPolicy.value.visibility.clauses, [ + [ + { kind: 'principal', principalId: 'principal:outsider' }, + { kind: 'principal', principalId: 'principal:packed' }, + ], + ]); + const outsiderOrRead = await successfulToolCall( + origin, + outsiderToken, + 158, + 'get_entity', + { entityId: 'Q2' }, + ); + assert.match(JSON.stringify(outsiderOrRead), /"id":"Q2"/u); + const outsiderRead = await mcp(origin, outsiderToken, 135, 'tools/call', { + name: 'get_entity', + arguments: { entityId: 'Q900' }, + }); + assert.ok( + outsiderRead.error || outsiderRead.result?.isError === true, + JSON.stringify(outsiderRead), + ); + const laggedHealth = await mcp(origin, token, 5, 'tools/call', { + name: 'search_admin', + arguments: { action: 'health' }, + }); + assert.equal( + laggedHealth.result?.isError, + false, + JSON.stringify(laggedHealth), + ); + const lagged = laggedHealth.result.structuredContent.materialization; + assert.ok( + lagged.sourceHighWatermark > lagged.activeAppliedWatermark, + JSON.stringify(lagged), + ); + const materialized = await mcp(origin, token, 11, 'tools/call', { + name: 'search_admin', + arguments: { + action: 'materialize', + limit: 100, + options: { maxRebuildRoots: 100 }, + }, + }); + assert.equal( + materialized.result?.isError, + false, + JSON.stringify(materialized), + ); + assert.ok( + materialized.result.structuredContent.completed >= 1, + JSON.stringify(materialized), + ); + const materializedReplay = await mcp(origin, token, 12, 'tools/call', { + name: 'search_admin', + arguments: { + action: 'materialize', + limit: 100, + options: { maxRebuildRoots: 100 }, + }, + }); + assert.equal( + materializedReplay.result?.isError, + false, + JSON.stringify(materializedReplay), + ); + assert.equal(materializedReplay.result.structuredContent.completed, 0); + const caughtUpHealth = await mcp(origin, token, 13, 'tools/call', { + name: 'search_admin', + arguments: { action: 'health' }, + }); + assert.equal( + caughtUpHealth.result.structuredContent.materialization + .activeAppliedWatermark, + caughtUpHealth.result.structuredContent.materialization.sourceHighWatermark, + ); + const semanticEstimate = await successfulToolCall( + origin, + token, + 214, + 'search_admin', + { + action: 'semantic-estimate', + options: { + configurationId: 'packed-openai-sqlite-v1', + policy: { mode: 'asap', maxBatchesPerRun: 1 }, + }, + }, + ); + await successfulToolCall(origin, token, 215, 'search_admin', { + action: 'semantic-approve', + options: { planId: semanticEstimate.planId }, + }); + const secondCreated = await mcp(origin, token, 14, 'tools/call', { + name: 'create_item', + arguments: { + idempotencyKey: 'packed-restart-item-v2', + authoredText: 'Create a second item to prove restart catch-up.', + data: { + id: 'Q901', + labels: { + en: { language: 'en', value: 'Packed restart item two' }, + }, + }, + }, + }); + assert.equal( + secondCreated.result?.isError, + false, + JSON.stringify(secondCreated), + ); + const secondLaggedHealth = await mcp(origin, token, 15, 'tools/call', { + name: 'search_admin', + arguments: { action: 'health' }, + }); + const secondLagged = + secondLaggedHealth.result.structuredContent.materialization; + assert.ok( + secondLagged.sourceHighWatermark > secondLagged.activeAppliedWatermark, + JSON.stringify(secondLagged), + ); + const laggedStatus = await mcp(origin, token, 10, 'tools/call', { + name: 'gnolith_status', + arguments: {}, + }); + assert.equal( + laggedStatus.result?.isError, + false, + JSON.stringify(laggedStatus), + ); + assert.equal(laggedStatus.result.structuredContent.lexicalReady, false); + const corpusGeneration = secondLagged.activeCorpusGeneration; + await terminate(child); + child = spawn( + process.execPath, + [ + join(fixture, 'node_modules', '@gnolith', 'workshop', 'dist', 'cli.js'), + 'serve', + '--config', + join(fixture, 'config.json'), + ], + { cwd: fixture, stdio: 'ignore' }, + ); + await waitForLive(origin, child); + const initiallyUnready = await fetch(`${origin}/health/ready`, { + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(initiallyUnready.status, 503); + assert.equal((await initiallyUnready.json()).lexicalReady, false); + const restartHealth = await mcp(origin, token, 6, 'tools/call', { + name: 'search_admin', + arguments: { action: 'health' }, + }); + assert.equal( + restartHealth.result.structuredContent.materialization + .activeCorpusGeneration, + corpusGeneration, + ); + assert.ok( + restartHealth.result.structuredContent.materialization.sourceHighWatermark > + restartHealth.result.structuredContent.materialization + .activeAppliedWatermark, + ); + const recovered = await waitForReady(origin, token, child); + assert.equal(recovered.lexicalReady, true); + assert.equal(recovered.semanticState.ready, true); + assert.equal(recovered.semanticState.diagnostic, null); + const outsiderOrReadAfterRestart = await successfulToolCall( + origin, + outsiderToken, + 159, + 'get_entity', + { entityId: 'Q2' }, + ); + assert.match(JSON.stringify(outsiderOrReadAfterRestart), /"id":"Q2"/u); + const outsiderOrSearchAfterRestart = await successfulToolCall( + origin, + outsiderToken, + 160, + 'search', + { + text: 'orchard heliotrope', + kinds: ['item'], + limit: 20, + }, + ); + assert.ok( + outsiderOrSearchAfterRestart.items.some((entry) => entry.sourceId === 'Q2'), + JSON.stringify(outsiderOrSearchAfterRestart), + ); + const semanticOnly = await successfulToolCall(origin, token, 216, 'search', { + text: 'meaning that has no lexical token overlap', + kinds: ['item'], + limit: 20, + }); + assert.ok( + semanticOnly.items.some((entry) => entry.sourceId === 'Q900'), + JSON.stringify(semanticOnly), + ); + const recoveredHealth = await mcp(origin, token, 7, 'tools/call', { + name: 'search_admin', + arguments: { action: 'health' }, + }); + assert.equal( + recoveredHealth.result.structuredContent.materialization + .activeCorpusGeneration, + corpusGeneration, + ); + assert.equal( + recoveredHealth.result.structuredContent.materialization + .activeAppliedWatermark, + recoveredHealth.result.structuredContent.materialization + .sourceHighWatermark, + ); + assert.deepEqual(await (await fetch(`${origin}/health/live`)).json(), { + status: 'live', + }); + const app = await fetch(`${origin}/app`); + assert.equal(app.status, 200); + const appBytes = Buffer.from(await app.arrayBuffer()); + assert.match(appBytes.toString('utf8'), /Packed Waystone/u); + const slashApp = await fetch(`${origin}/app/`); + assert.equal(slashApp.status, 200); + assert.equal( + slashApp.headers.get('content-type'), + app.headers.get('content-type'), + ); + assert.deepEqual(Buffer.from(await slashApp.arrayBuffer()), appBytes); + assert.equal((await fetch(`${origin}/`)).status, 404); + assert.equal((await fetch(`${origin}/app/%2e%2e/secret`)).status, 404); + for (const target of [ + '/app/%2e%2e/mcp', + '/app/%2E%2E/health/live', + '/app%2f..%2fmcp', + ]) { + const response = await rawHttp(port, target); + assert.match(response.statusLine, / 400 /u, target); + assert.match(response.body, /bad_request/u, target); + } + const initialize = await fetch(`${origin}/mcp`, { + method: 'POST', + headers: { + authorization: `Bearer ${token}`, + accept: 'application/json, text/event-stream', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { protocolVersion: '2025-03-26' }, + }), + }); + const body = await initialize.json(); + assert.equal(body.result.serverInfo.name, 'gnolith'); + assert.equal(body.result.serverInfo.version, '0.5.0'); + const restartedTools = await mcp(origin, token, 8, 'tools/list', {}); + assert.equal(restartedTools.result.tools.length, 52); + const status = await mcp(origin, token, 3, 'tools/call', { + name: 'gnolith_status', + arguments: {}, + }); + assert.equal(status.result?.isError, false, JSON.stringify(status)); + assert.equal(status.result.structuredContent.blobReady, true); + assert.equal(status.result.structuredContent.semanticState.configured, true); + assert.equal(status.result.structuredContent.semanticState.revision, 1); + assert.match( + status.result.structuredContent.semanticState.fingerprint, + /^[a-f0-9]{64}$/u, + ); + const item = await mcp(origin, token, 9, 'tools/call', { + name: 'get_entity', + arguments: { entityId: 'Q900' }, + }); + assert.equal(item.result?.isError, false, JSON.stringify(item)); + assert.match(JSON.stringify(item.result.structuredContent), /"id":"Q900"/u); + assert.match( + JSON.stringify(item.result.structuredContent), + /Packed restart item/u, + ); + const populated = JSON.parse( + execFileSync( + process.execPath, + [ + join(root, 'scripts', 'populate-public-backup-fixture.mjs'), + origin, + token, + ], + { cwd: root, encoding: 'utf8' }, + ), + ); + assert.equal(populated.format, 'gnolith-workshop-public-backup-fixture-v1'); + assert.equal(populated.q1Visibility.target.id, 'Q1'); + const q1PolicyRevision = ( + await successfulToolCall(origin, token, 180, 'gnolith_status', {}) + ).authorizationRevision; + const q1FailureArguments = { + version: 1, + action: 'visibility-policy-set', + expectedPolicyRevision: q1PolicyRevision, + expectedTargetRevision: 3, + visibility: { + version: 1, + clauses: [[{ kind: 'workspace', workspaceId: 'workspace:packed' }]], + }, + }; + for (const [id, expectedCode, argumentsValue] of [ + [ + 181, + 'conflict', + { + ...q1FailureArguments, + idempotencyKey: 'packed-q1-stale-v1', + target: { kind: 'entity', id: 'Q1' }, + }, + ], + [ + 182, + 'not_found', + { + ...q1FailureArguments, + idempotencyKey: 'packed-q1-missing-v1', + target: { kind: 'entity', id: 'Q999' }, + }, + ], + [ + 183, + 'validation_failed', + { + ...q1FailureArguments, + idempotencyKey: 'packed-q1-invalid-v1', + target: { kind: 'entity', id: 'Q0' }, + }, + ], + ]) { + const failed = await mcp(origin, token, id, 'tools/call', { + name: 'authorization_admin', + arguments: argumentsValue, + }); + assert.equal(toolErrorCode(failed), expectedCode, JSON.stringify(failed)); + } + const unauthorizedQ1 = await mcp(origin, outsiderToken, 184, 'tools/call', { + name: 'authorization_admin', + arguments: { + ...q1FailureArguments, + idempotencyKey: 'packed-q1-unauthorized-v1', + target: { kind: 'entity', id: 'Q1' }, + }, + }); + assert.equal( + toolErrorCode(unauthorizedQ1), + 'forbidden', + JSON.stringify(unauthorizedQ1), + ); + const allKindMaterialization = await successfulToolCall( + origin, + token, + 190, + 'search_admin', + { + action: 'materialize', + limit: 1_000, + options: { maxRebuildRoots: 1_000 }, + }, + ); + assert.ok(allKindMaterialization.claimed >= 7); + assert.equal( + allKindMaterialization.health.activeAppliedWatermark, + allKindMaterialization.health.sourceHighWatermark, + ); + const publicSearchFixtures = [ + ['item', 'Backup fixture subject', 'Q1'], + [ + 'resource', + 'Inline backup fixture resource payload', + 'resource:backup-fixture', + ], + [ + 'annotation', + 'Backup fixture annotation body', + 'annotation:backup-fixture', + ], + ['task', 'Complete the backup fixture task', populated.taskId], + ['memory', 'deterministic memory revision', 'backup-fixture-memory'], + ['prompt', 'deterministic prompt revision', populated.promptId], + ]; + for (const [ + index, + [kind, text, sourceId], + ] of publicSearchFixtures.entries()) { + const page = await successfulToolCall( + origin, + token, + 191 + index, + 'search', + { text, kinds: [kind], limit: 20 }, + ); + assert.ok( + page.items.some( + (entry) => entry.kind === kind && entry.sourceId === sourceId, + ), + JSON.stringify({ kind, text, sourceId, page }), + ); + } + const statementSearches = []; + for (const [index, text] of [ + 'Add', + 'fixture', + 'statement', + 'qualified', + ].entries()) { + statementSearches.push( + await successfulToolCall(origin, token, 210 + index, 'search', { + text, + kinds: ['statement'], + limit: 20, + }), + ); + } + assert.ok( + statementSearches.some((page) => + page.items.some( + (entry) => + entry.kind === 'statement' && + entry.sourceId === 'Q1$backup-fixture-statement', + ), + ), + JSON.stringify(statementSearches), + ); + const qdrantConfiguration = await successfulToolCall( + origin, + token, + 220, + 'search_admin', + { + action: 'configure', + options: { + expectedRevision: 0, + idempotencyKey: 'packed-openai-qdrant-v1', + configuration: { + version: 1, + id: 'packed-openai-qdrant-v1', + name: 'Packed OpenAI Qdrant', + provider: { + kind: 'openai-compatible', + endpoint: `${semanticMockOrigin}/v1/embeddings`, + model: 'text-embedding-packed', + dimensions: 4, + metric: 'cosine', + credentialSelector: 'provider:packed', + allowPrivateEndpoint: true, + redirectPolicy: 'error', + }, + vector: { + kind: 'qdrant', + endpoint: semanticMockOrigin, + collection: 'packed_vectors', + credentialSelector: 'qdrant:packed', + allowPrivateEndpoint: true, + redirectPolicy: 'error', + }, + }, + }, + }, + ); + assert.equal(qdrantConfiguration.configurationId, 'packed-openai-qdrant-v1'); + const qdrantPending = await successfulToolCall( + origin, + token, + 2201, + 'gnolith_status', + {}, + ); + assert.deepEqual(qdrantPending.semanticState.diagnostic, { + format: 'gnolith-workshop-semantic-diagnostic-v1', + version: 1, + code: 'materialization-pending', + retryable: true, + backend: 'qdrant', + repair: 'retry', + }); + const qdrantEstimate = await successfulToolCall( + origin, + token, + 221, + 'search_admin', + { + action: 'semantic-estimate', + options: { + configurationId: 'packed-openai-qdrant-v1', + policy: { mode: 'asap', maxBatchesPerRun: 2 }, + }, + }, + ); + await successfulToolCall(origin, token, 222, 'search_admin', { + action: 'semantic-approve', + options: { planId: qdrantEstimate.planId }, + }); + const qdrantReady = await waitForReady(origin, token, child); + assert.equal(qdrantReady.semanticState.ready, true); + assert.equal(qdrantReady.semanticState.diagnostic, null); + await terminate(child); + child = spawn( + process.execPath, + [ + join(fixture, 'node_modules', '@gnolith', 'workshop', 'dist', 'cli.js'), + 'serve', + '--config', + join(fixture, 'config.json'), + ], + { cwd: fixture, stdio: 'ignore' }, + ); + await waitForLive(origin, child); + const qdrantRestarted = await waitForReady(origin, token, child); + assert.equal(qdrantRestarted.semanticState.ready, true); + assert.ok( + [...semanticMockPoints.values()].some( + (point) => point.payload?.sourceId === 'Q1', + ), + JSON.stringify( + [...semanticMockPoints.values()].map( + (point) => point.payload?.sourceId ?? null, + ), + ), + ); + const qdrantSemanticOnly = await successfulToolCall( + origin, + token, + 223, + 'search', + { + text: 'another phrase with no lexical overlap', + kinds: ['item'], + limit: 20, + }, + ); + assert.ok( + qdrantSemanticOnly.items.some((entry) => entry.sourceId === 'Q1'), + JSON.stringify(qdrantSemanticOnly), + ); + const secondBackupTask = await successfulToolCall( + origin, + token, + 200, + 'create_task', + { + idempotencyKey: 'backup-fixture-second-task-v1', + description: 'Complete a second independent backup task', + prompt: 'Return the second deterministic fixture outcome.', + }, + ); + const secondBackupClaim = await successfulToolCall( + origin, + token, + 201, + 'claim_task', + { taskId: secondBackupTask.id, leaseSeconds: 300 }, + ); + await successfulToolCall(origin, token, 202, 'complete_task', { + taskId: secondBackupTask.id, + leaseToken: secondBackupClaim.leaseToken, + expectedRevision: + secondBackupClaim.task?.revision ?? secondBackupClaim.revision, + idempotencyKey: 'backup-fixture-second-task-complete-v1', + result: 'Second backup fixture completed.', + outcomeKind: 'success', + }); + await successfulToolCall(origin, token, 203, 'search_admin', { + action: 'materialize', + limit: 100, + options: { maxRebuildRoots: 100 }, + }); + const unavailableSemantic = await successfulToolCall( + origin, + token, + 224, + 'search_admin', + { + action: 'configure', + options: { + expectedRevision: 0, + idempotencyKey: 'packed-unavailable-semantic-v1', + configuration: { + version: 1, + id: 'packed-unavailable-semantic-v1', + name: 'Packed unavailable semantic endpoint', + provider: { + kind: 'openai-compatible', + endpoint: 'http://127.0.0.1:1/v1/embeddings', + model: 'text-embedding-packed', + dimensions: 4, + metric: 'cosine', + credentialSelector: 'provider:packed', + allowPrivateEndpoint: true, + redirectPolicy: 'error', + }, + vector: { kind: 'sqlite' }, + }, + }, + }, + ); + assert.equal(unavailableSemantic.ready, false); + const degraded = await fetch(`${origin}/health/ready`, { + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(degraded.status, 503); + const degradedBody = await degraded.json(); + assert.equal(degradedBody.lexicalReady, true); + assert.deepEqual(degradedBody.semanticState.diagnostic, { + format: 'gnolith-workshop-semantic-diagnostic-v1', + version: 1, + code: 'provider-unavailable', + retryable: true, + backend: 'sqlite', + repair: 'retry', + }); + assert.doesNotMatch(JSON.stringify(degradedBody), /127\.0\.0\.1|secret/u); + await terminate(child); + child = spawn( + process.execPath, + [ + join(fixture, 'node_modules', '@gnolith', 'workshop', 'dist', 'cli.js'), + 'serve', + '--config', + join(fixture, 'config.json'), + ], + { cwd: fixture, stdio: 'ignore' }, + ); + await waitForLive(origin, child); + const degradedAfterRestart = await fetch(`${origin}/health/ready`, { + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(degradedAfterRestart.status, 503); + assert.equal((await degradedAfterRestart.json()).lexicalReady, true); + await new Promise((resolvePromise) => setTimeout(resolvePromise, 500)); + assert.equal(child.exitCode, null, 'bad semantic config restart-looped'); + await terminate(child); + const ownInspection = JSON.parse( + execFileSync( + process.execPath, + [ + join(root, 'scripts', 'inspect-packed-owner.mjs'), + join(fixture, 'adapter.mjs'), + ], + { cwd: root, encoding: 'utf8' }, + ), + ); + assert.ok(ownInspection.ownerInspection.taskCount >= 2); + assert.ok(ownInspection.ownerInspection.memoryCount >= 1); + assert.ok(ownInspection.ownerInspection.promptCount >= 1); + assert.equal(ownInspection.fullInspection.valid, true); + assert.deepEqual( + ownInspection.taskIdentities + .map(({ idempotencyKey }) => idempotencyKey) + .sort(), + [ + 'installation:packed\u0000workspace:packed\u0000principal:packed\u0000backup-fixture-second-task-v1', + 'installation:packed\u0000workspace:packed\u0000principal:packed\u0000backup-fixture-task-v1', + ], + ); + process.stdout.write( + `${JSON.stringify({ packedProcess: 'ok', catalogDigest: 'af455b4d6ab3560bca9c0ab48e9db7bd3cbcfa166ace875578d88d182069071f' })}\n`, + ); +} finally { + if (child?.exitCode === null) await terminate(child); + if (semanticMock) + await new Promise((resolvePromise, reject) => + semanticMock.close((error) => (error ? reject(error) : resolvePromise())), + ); + await rm(fixture, { + recursive: true, + force: true, + maxRetries: 10, + retryDelay: 100, + }); + await rm(tarball, { force: true }); +} + +async function startSemanticMock() { + const points = new Map(); + // The Node listener owns the returned promise through its response lifecycle. + // eslint-disable-next-line @typescript-eslint/no-misused-promises + const server = createHttpServer(async (request, response) => { + try { + const url = new URL(request.url ?? '/', 'http://127.0.0.1'); + const body = await readNodeBody(request); + if (url.pathname === '/v1/embeddings' && request.method === 'POST') { + assert.equal( + request.headers.authorization, + 'Bearer packed-openai-secret', + ); + const input = JSON.parse(body); + assert.equal(input.dimensions, 4); + assert.ok(Array.isArray(input.input)); + return json(response, 200, { + data: input.input.map((_, index) => ({ + index, + embedding: [1, 0, 0, 0], + })), + usage: { total_tokens: input.input.length }, + }); + } + const collection = '/collections/packed_vectors'; + if (url.pathname.startsWith(collection)) { + assert.equal(request.headers['api-key'], 'packed-qdrant-secret'); + if (url.pathname === collection && request.method === 'GET') + return json(response, 200, { + result: { + config: { + params: { vectors: { size: 4, distance: 'Cosine' } }, + }, + }, + }); + if ( + url.pathname === `${collection}/points` && + request.method === 'PUT' + ) { + for (const point of JSON.parse(body).points ?? []) + points.set(point.id, point); + return json(response, 200, { result: { status: 'completed' } }); + } + if ( + url.pathname === `${collection}/points/delete` && + request.method === 'POST' + ) { + const deletion = JSON.parse(body); + if (Array.isArray(deletion.points)) + for (const point of deletion.points) points.delete(point); + else points.clear(); + return json(response, 200, { result: { status: 'completed' } }); + } + if ( + url.pathname === `${collection}/points/query` && + request.method === 'POST' + ) + return json(response, 200, { + result: { + points: [...points.values()] + .sort((left, right) => + left.payload?.sourceId === 'Q1' + ? -1 + : right.payload?.sourceId === 'Q1' + ? 1 + : String(left.id).localeCompare(String(right.id)), + ) + .map((point) => ({ + id: point.id, + score: point.payload?.sourceId === 'Q1' ? 2 : 1, + payload: point.payload, + })), + }, + }); + } + return json(response, 404, { error: 'unexpected mock route' }); + } catch (error) { + return json(response, 500, { + error: error instanceof Error ? error.message : 'mock failure', + }); + } + }); + await new Promise((resolvePromise, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', () => { + server.off('error', reject); + resolvePromise(); + }); + }); + const address = server.address(); + assert.ok(address && typeof address === 'object'); + return { + server, + origin: `http://127.0.0.1:${address.port}`, + points, + }; +} + +async function readNodeBody(request) { + let value = ''; + for await (const chunk of request) value += String(chunk); + return value; +} + +function json(response, status, value) { + response.writeHead(status, { 'content-type': 'application/json' }); + response.end(JSON.stringify(value)); +} + +async function mcp(origin, token, id, method, params) { + return ( + await fetch(`${origin}/mcp`, { + method: 'POST', + headers: { + authorization: `Bearer ${token}`, + accept: 'application/json, text/event-stream', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id, + method, + params, + }), + }) + ).json(); +} + +async function successfulToolCall(origin, token, id, name, argumentsValue) { + const response = await mcp(origin, token, id, 'tools/call', { + name, + arguments: argumentsValue, + }); + assert.equal(response.result?.isError, false, JSON.stringify(response)); + return response.result.structuredContent; +} + +function toolErrorCode(response) { + return ( + response.error?.data?.code ?? + response.result?.structuredContent?.code ?? + (response.error?.code === -32001 ? 'forbidden' : null) ?? + null + ); +} + +async function mcpHttpStatus(origin, token, id, name, argumentsValue) { + const response = await fetch(`${origin}/mcp`, { + method: 'POST', + headers: { + authorization: `Bearer ${token}`, + accept: 'application/json, text/event-stream', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id, + method: 'tools/call', + params: { name, arguments: argumentsValue }, + }), + }); + return response.status; +} + +async function terminate(processHandle) { + if (processHandle.exitCode !== null) return; + const exited = new Promise((resolvePromise) => + processHandle.once('exit', resolvePromise), + ); + processHandle.kill('SIGTERM'); + const completed = await Promise.race([ + exited.then(() => true), + new Promise((resolvePromise) => + setTimeout(() => resolvePromise(false), 10_000), + ), + ]); + if (completed) return; + if (process.platform === 'win32') { + try { + execFileSync( + 'taskkill', + ['/pid', String(processHandle.pid), '/t', '/f'], + { + stdio: 'ignore', + }, + ); + } catch (error) { + if (processHandle.kill(0)) throw error; + return; + } + } else if (!processHandle.kill('SIGKILL')) return; + await Promise.race([ + exited, + new Promise((resolvePromise) => setTimeout(resolvePromise, 2_000)), + ]); +} + +async function availablePort() { + const server = createServer(); + await new Promise((resolvePromise, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', resolvePromise); + }); + const address = server.address(); + assert(address && typeof address === 'object'); + await new Promise((resolvePromise, reject) => + server.close((error) => (error ? reject(error) : resolvePromise())), + ); + return address.port; +} + +async function rawHttp(port, target) { + const response = await new Promise((resolvePromise, rejectPromise) => { + const socket = connect(port, '127.0.0.1'); + let value = ''; + socket.setEncoding('utf8'); + socket.once('error', rejectPromise); + socket.on('data', (chunk) => { + value += String(chunk); + }); + socket.once('end', () => resolvePromise(value)); + socket.once('connect', () => { + socket.end( + `GET ${target} HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nConnection: close\r\n\r\n`, + ); + }); + }); + const separator = response.indexOf('\r\n\r\n'); + return { + statusLine: response.slice(0, response.indexOf('\r\n')), + body: separator < 0 ? '' : response.slice(separator + 4), + }; +} + +async function waitForLive(origin, processHandle) { + for (let attempt = 0; attempt < 100; attempt += 1) { + if (processHandle.exitCode !== null) + throw new Error(`packed process exited ${processHandle.exitCode}`); + try { + const response = await fetch(`${origin}/health/live`); + if (response.ok) return; + } catch { + // The listener is not ready yet. + } + await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); + } + throw new Error('packed process did not become live'); +} + +async function waitForReady(origin, token, processHandle) { + let lastBody = null; + for (let attempt = 0; attempt < 100; attempt += 1) { + if (processHandle.exitCode !== null) + throw new Error(`packed process exited ${processHandle.exitCode}`); + const response = await fetch(`${origin}/health/ready`, { + headers: { authorization: `Bearer ${token}` }, + }); + if (response.ok) return response.json(); + assert.equal(response.status, 503); + lastBody = await response.json(); + await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); + } + const health = await mcp(origin, token, 999_999, 'tools/call', { + name: 'search_admin', + arguments: { action: 'health' }, + }); + throw new Error( + `packed process did not recover readiness: ${JSON.stringify({ readiness: lastBody, search: health })}`, + ); +} + +function canonicalize(value) { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) + return `[${value.map((entry) => canonicalize(entry)).join(',')}]`; + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`) + .join(',')}}`; +} diff --git a/scripts/performance-check.mjs b/scripts/performance-check.mjs index 752bc06..de84533 100644 --- a/scripts/performance-check.mjs +++ b/scripts/performance-check.mjs @@ -10,12 +10,14 @@ import { createTaprootHostWriteCapability, initializeTaproot, } from '@gnolith/taproot'; -import { createWorkshopMcpServer } from '../dist/mcp.js'; -import { workshopMigrations } from '../dist/migrations.js'; import { - createWorkshopRuntime, - createWorkshopSearchIntegrationV1, -} from '../dist/server.js'; + CATALOG_DIGEST, + createGnolithMcpServer, + operationCatalog, +} from '../dist/mcp.js'; +import { workshopMigrations } from '../dist/migrations.js'; +import { createWorkshopSearchIntegrationV1 } from '../dist/server.js'; +import { createWorkshopRuntime } from '../dist/server/context.js'; const miniflare = new Miniflare({ modules: true, @@ -46,10 +48,10 @@ try { workspaceIds: ['performance:workspace'], capabilities: [ 'read', - 'task-write', - 'memory-write', - 'prompt-write', - 'knowledge-write', + 'task:write', + 'memory:write', + 'prompt:write', + 'knowledge:write', 'search:admin', 'admin', ], @@ -136,7 +138,17 @@ try { }, authorizationContext, ); - const mcp = createWorkshopMcpServer(runtime); + const mcp = createGnolithMcpServer({ + version: '0.5.0', + operations: { + catalog: operationCatalog, + catalogDigest: CATALOG_DIGEST, + async call() { + throw new Error('The performance harness does not dispatch tools'); + }, + }, + authenticate: async () => authorizationContext, + }); const mcpRequest = (id, method) => new Request('https://performance.example/mcp', { method: 'POST', @@ -183,15 +195,20 @@ try { for (const [name, value] of Object.entries(metrics)) { assert.ok(value < 2_000, `${name} exceeded the 2s local regression bound`); } - const uiFiles = readdirSync('dist/ui', { + const runtimeFiles = readdirSync('dist', { recursive: true, withFileTypes: true, }) .filter((entry) => entry.isFile() && entry.name.endsWith('.js')) .map((entry) => readFileSync(`${entry.parentPath}/${entry.name}`)); - const uiBytes = uiFiles.reduce((sum, file) => sum + file.byteLength, 0); - const uiGzipBytes = gzipSync(Buffer.concat(uiFiles)).byteLength; - console.log(JSON.stringify({ ...metrics, uiBytes, uiGzipBytes })); + const runtimeJsBytes = runtimeFiles.reduce( + (sum, file) => sum + file.byteLength, + 0, + ); + const runtimeJsGzipBytes = gzipSync(Buffer.concat(runtimeFiles)).byteLength; + console.log( + JSON.stringify({ ...metrics, runtimeJsBytes, runtimeJsGzipBytes }), + ); } finally { await miniflare.dispose(); } diff --git a/scripts/populate-public-backup-fixture.mjs b/scripts/populate-public-backup-fixture.mjs new file mode 100644 index 0000000..2768b80 --- /dev/null +++ b/scripts/populate-public-backup-fixture.mjs @@ -0,0 +1,371 @@ +/* global fetch */ +import assert from 'node:assert/strict'; +import { Buffer } from 'node:buffer'; +import { createHash } from 'node:crypto'; + +const [origin, token] = process.argv.slice(2); +if (!origin || !token) { + throw new Error( + 'usage: node scripts/populate-public-backup-fixture.mjs ', + ); +} + +let requestId = 1; +const request = async (name, argumentsValue) => { + const response = await fetch(`${origin.replace(/\/$/u, '')}/mcp`, { + method: 'POST', + headers: { + authorization: `Bearer ${token}`, + accept: 'application/json, text/event-stream', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: requestId++, + method: 'tools/call', + params: { name, arguments: argumentsValue }, + }), + }); + assert.equal(response.status, 200, `${name}: HTTP ${response.status}`); + return response.json(); +}; +const call = async (name, argumentsValue) => { + const body = await request(name, argumentsValue); + assert.equal(body.result?.isError, false, `${name}: ${JSON.stringify(body)}`); + return body.result.structuredContent; +}; + +const initialStatus = await call('gnolith_status', {}); +const workspaceId = initialStatus.activeWorkspaceId; +assert.equal(typeof workspaceId, 'string'); + +await call('create_property', { + idempotencyKey: 'backup-fixture-property-p1-v1', + authoredText: 'Create the fixture source property.', + data: { + id: 'P1', + datatype: 'string', + labels: { en: { language: 'en', value: 'Fixture source' } }, + }, +}); +await call('create_item', { + idempotencyKey: 'backup-fixture-item-q1-v1', + authoredText: 'Create the fixture subject.', + data: { + id: 'Q1', + labels: { en: { language: 'en', value: 'Backup fixture subject' } }, + }, +}); + +const statementId = 'Q1$backup-fixture-statement'; +const valueSnak = { + snaktype: 'value', + property: 'P1', + datatype: 'string', + datavalue: { value: 'fixture value', type: 'string' }, +}; +await call('mutate_entity', { + entityId: 'Q1', + expectedRevision: 1, + idempotencyKey: 'backup-fixture-statement-v1', + mutations: [ + { + kind: 'statement', + authoredText: 'Add the fixture statement.', + value: { + type: 'add-statement', + statement: { + id: statementId, + type: 'statement', + text: 'Add the fixture statement.', + rank: 'normal', + mainsnak: valueSnak, + qualifiers: {}, + 'qualifiers-order': [], + references: [], + }, + }, + }, + ], +}); +await call('mutate_entity', { + entityId: 'Q1', + expectedRevision: 2, + idempotencyKey: 'backup-fixture-statement-details-v1', + mutations: [ + { + kind: 'qualifier', + authoredText: 'Qualify the fixture statement.', + value: { + type: 'add-qualifier', + statementId, + snak: { + ...valueSnak, + datavalue: { value: 'qualified', type: 'string' }, + }, + text: 'Qualify the fixture statement.', + }, + }, + { + kind: 'reference', + authoredText: 'Reference the fixture statement.', + value: { + type: 'add-reference', + statementId, + reference: { + hash: 'backup-fixture-reference', + snaks: { + P1: [ + { + ...valueSnak, + datavalue: { value: 'source', type: 'string' }, + }, + ], + }, + 'snaks-order': ['P1'], + }, + text: 'Reference the fixture statement.', + }, + }, + { + kind: 'rank', + authoredText: 'Prefer the fixture statement.', + value: { + type: 'set-statement-rank', + statementId, + rank: 'preferred', + text: 'Prefer the fixture statement.', + }, + }, + ], +}); + +const resourceText = 'Inline backup fixture resource payload.'; +const resourceBytes = Buffer.from(resourceText, 'utf8'); +await call('create_resource', { + idempotencyKey: 'backup-fixture-resource-v1', + data: { + id: 'resource:backup-fixture', + itemId: 'Q1', + title: 'Backup fixture resource', + payload: { kind: 'inline-text', text: resourceText }, + mediaType: 'text/plain', + language: 'en', + integrity: { + algorithm: 'sha256', + digest: createHash('sha256').update(resourceBytes).digest('hex'), + byteLength: resourceBytes.byteLength, + }, + }, +}); +await call('create_annotation', { + idempotencyKey: 'backup-fixture-annotation-v1', + data: { + id: 'annotation:backup-fixture', + body: { + kind: 'text', + text: 'Backup fixture annotation body.', + mediaType: 'text/plain', + language: 'en', + }, + target: { + kind: 'resource', + sourceId: 'resource:backup-fixture', + selector: { type: 'FragmentSelector', value: 'line=1' }, + }, + motivation: 'commenting', + }, +}); + +const task = await call('create_task', { + idempotencyKey: 'backup-fixture-task-v1', + description: 'Complete the backup fixture task', + prompt: 'Return the deterministic fixture outcome.', + data: { fixture: true }, +}); +const taskId = task.id ?? task.taskId; +assert.equal(typeof taskId, 'string'); +const claims = await Promise.allSettled([ + call('claim_task', { taskId, leaseSeconds: 300 }), + call('claim_task', { taskId, leaseSeconds: 300 }), +]); +const winner = claims.find((entry) => entry.status === 'fulfilled')?.value; +assert.ok(winner?.leaseToken, 'one task claim must win'); +const completionInput = { + taskId, + leaseToken: winner.leaseToken, + expectedRevision: winner.task?.revision ?? winner.revision, + idempotencyKey: 'backup-fixture-task-complete-v1', + result: 'Backup fixture completed.', + outcomeKind: 'success', +}; +const completion = await call('complete_task', completionInput); +assert.deepEqual(await call('complete_task', completionInput), completion); + +const memory = await call('upsert_memory', { + slug: 'backup-fixture-memory', + idempotencyKey: 'backup-fixture-memory-v1', + description: 'Backup fixture memory', + content: 'First deterministic memory revision.', +}); +await call('upsert_memory', { + slug: memory.slug, + expectedRevision: memory.revision, + idempotencyKey: 'backup-fixture-memory-v2', + description: 'Backup fixture memory', + content: 'Second deterministic memory revision.', +}); +const prompt = await call('create_prompt', { + idempotencyKey: 'backup-fixture-prompt-v1', + name: 'backup-fixture-prompt', + promptText: 'First deterministic prompt revision.', + data: { fixture: true }, +}); +await call('update_prompt', { + promptId: prompt.id, + expectedRevision: prompt.revision, + idempotencyKey: 'backup-fixture-prompt-v2', + patch: { promptText: 'Second deterministic prompt revision.' }, +}); + +for (let index = 0; index < 25; index += 1) { + const entityNumber = 100 + index; + await call('create_item', { + idempotencyKey: `backup-fixture-lag-q${entityNumber}-v1`, + authoredText: `Create lag fixture item ${entityNumber}.`, + data: { + id: `Q${entityNumber}`, + labels: { + en: { + language: 'en', + value: `Backup fixture lag item ${entityNumber}`, + }, + }, + }, + }); +} + +let policyRevision = (await call('gnolith_status', {})).authorizationRevision; +const administer = async (action) => { + const result = await call('authorization_admin', { + version: 1, + expectedPolicyRevision: policyRevision, + ...action, + }); + policyRevision = result.policyRevision; + return result; +}; +await administer({ + action: 'principal-create', + idempotencyKey: 'backup-fixture-principal-v1', + principal: { principalId: 'principal:backup-fixture', enabled: true }, +}); +await administer({ + action: 'workspace-membership-add', + idempotencyKey: 'backup-fixture-membership-v1', + principalId: 'principal:backup-fixture', + workspaceId, +}); +for (const capability of ['read', 'admin', 'search:admin']) { + await administer({ + action: 'workspace-grant-add', + idempotencyKey: `backup-fixture-grant-${capability}-v1`, + principalId: 'principal:backup-fixture', + workspaceId, + capability, + }); +} +const delegatedBytes = Buffer.from( + Uint8Array.from({ length: 32 }, (_, index) => index + 31), +); +const delegatedToken = delegatedBytes.toString('base64url'); +const delegatedDigest = createHash('sha256') + .update(Buffer.from('gnolith-local-bearer-v1\0', 'utf8')) + .update(delegatedBytes) + .digest('hex'); +await administer({ + action: 'credential-register', + idempotencyKey: 'backup-fixture-credential-v1', + credential: { + credentialId: 'credential:backup-fixture', + tokenDigest: delegatedDigest, + principalId: 'principal:backup-fixture', + workspaceId, + }, +}); +const auditBeforeVisibility = await call('authorization_admin', { + version: 1, + action: 'status', +}); +const q1VisibilityArguments = { + version: 1, + action: 'visibility-policy-set', + expectedPolicyRevision: policyRevision, + idempotencyKey: 'backup-fixture-q1-policy-v1', + target: { kind: 'entity', id: 'Q1' }, + expectedTargetRevision: 3, + visibility: { + version: 1, + clauses: [[{ kind: 'workspace', workspaceId }]], + }, +}; +const q1Visibility = await call('authorization_admin', q1VisibilityArguments); +policyRevision = q1Visibility.policyRevision; +const q1VisibilityReplay = await call( + 'authorization_admin', + q1VisibilityArguments, +); +assert.equal(q1VisibilityReplay.status, 'replayed'); +assert.equal(q1VisibilityReplay.auditId, q1Visibility.auditId); +const q1VisibilityConflict = await request('authorization_admin', { + ...q1VisibilityArguments, + visibility: { version: 1, clauses: [] }, +}); +assert.equal( + q1VisibilityConflict.error?.data?.code ?? + q1VisibilityConflict.result?.structuredContent?.code, + 'conflict', +); +const auditAfterVisibility = await call('authorization_admin', { + version: 1, + action: 'status', +}); +assert.equal( + auditAfterVisibility.value.audits, + auditBeforeVisibility.value.audits + 1, +); +const q1AfterVisibility = await call('get_entity', { entityId: 'Q1' }); +assert.equal(q1AfterVisibility.entity.lastrevid, 4); +const q1VisibilityRevision = await call('entity_revision', { + entityId: 'Q1', + revision: 4, +}); +assert.match(JSON.stringify(q1VisibilityRevision), /"id":"Q1"/u); +const q1EffectivePolicy = await call('authorization_admin', { + version: 1, + action: 'effective-policy', + target: { kind: 'entity', id: 'Q1' }, +}); +assert.equal(q1EffectivePolicy.value.sourceRevision, 4); +assert.equal( + q1EffectivePolicy.value.sourcePolicyRevision, + q1Visibility.policyRevision, +); +assert.deepEqual(q1EffectivePolicy.value.visibility, { + version: 1, + clauses: [[{ kind: 'workspace', workspaceId }]], +}); + +process.stdout.write( + `${JSON.stringify({ + format: 'gnolith-workshop-public-backup-fixture-v1', + taskId, + promptId: prompt.id, + completionInput, + delegatedToken, + q1Visibility, + q1EffectivePolicy, + catalogDigest: + 'af455b4d6ab3560bca9c0ab48e9db7bd3cbcfa166ace875578d88d182069071f', + })}\n`, +); diff --git a/scripts/prepare-candidate.mjs b/scripts/prepare-candidate.mjs new file mode 100644 index 0000000..820cf2b --- /dev/null +++ b/scripts/prepare-candidate.mjs @@ -0,0 +1,108 @@ +import { createHash } from 'node:crypto'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { spawnSync } from 'node:child_process'; +import { resolve } from 'node:path'; + +const root = resolve(import.meta.dirname, '..'); +const manifest = JSON.parse(await readFile(resolve(root, 'package.json'))); +const peerArtifacts = JSON.parse( + await readFile(resolve(root, 'docs', 'peer-artifacts-v1.json'), 'utf8'), +); +const head = run('git', ['rev-parse', 'HEAD']).trim(); +if (run('git', ['status', '--porcelain']).trim()) + throw new Error('Candidate preparation requires a clean committed tree'); +if (!/^[a-f0-9]{40}$/u.test(head)) + throw new Error('Candidate commit is invalid'); +const output = resolve( + root, + '.artifacts', + `${manifest.name.split('/').at(-1)}-${manifest.version}-${head}`, +); +await mkdir(output, { recursive: true }); +run('npm', ['pack', '--pack-destination', output], { stdout: 'inherit' }); +const artifact = resolve(output, `gnolith-workshop-${manifest.version}.tgz`); +const sbomText = run('npm', [ + 'sbom', + '--omit=dev', + '--sbom-format', + 'cyclonedx', +]); +const sbom = resolve( + output, + `gnolith-workshop-${manifest.version}.sbom.cdx.json`, +); +await writeFile(sbom, `${sbomText.trim()}\n`); +const artifactSha256 = await sha256(artifact); +const sbomSha256 = await sha256(sbom); +const migrationIds = [ + '0001_workshop', + '0002_revisions', + '0003_resumable_onboarding', + '0004_authorization', + '0005_cursor_snapshots', + '0006_canonical_search_domains', + '0007_task_leases', + '0008_bootstrap_credentials', + '0009_operation_receipts', + '0010_durable_mutation_receipts', + '0011_administration_control_plane', +]; +const inventory = { + format: 'gnolith-workshop-candidate-v1', + package: manifest.name, + version: manifest.version, + commit: head, + artifact, + artifactSha256, + sbom, + sbomSha256, + catalogDigest: + 'af455b4d6ab3560bca9c0ab48e9db7bd3cbcfa166ace875578d88d182069071f', + peerArtifacts, + schemaVersion: 11, + migrationNamespace: '@gnolith/workshop', + migrations: await Promise.all( + migrationIds.map(async (id) => ({ + id, + checksum: `sha256:${await migrationChecksum( + resolve(root, 'migrations', `${id}.sql`), + )}`, + })), + ), + configSchema: 'schemas/workshop-config-v1.json', + blobFactory: + '@gnolith/workshop/backup#createLocalFilesystemResourceBlobPortsV1', + mcpServerName: 'gnolith', + createdAt: new Date().toISOString(), +}; +const inventoryPath = resolve(output, 'candidate-inventory.json'); +await writeFile(inventoryPath, `${JSON.stringify(inventory, null, 2)}\n`); +await writeFile( + resolve(output, 'SHA256SUMS'), + `${artifactSha256} ${artifact.split(/[\\/]/u).at(-1)}\n${sbomSha256} ${sbom.split(/[\\/]/u).at(-1)}\n${await sha256(inventoryPath)} candidate-inventory.json\n`, +); +process.stdout.write(`${JSON.stringify({ ...inventory, inventoryPath })}\n`); + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: root, + encoding: 'utf8', + shell: process.platform === 'win32', + ...options, + }); + if (result.status !== 0) + throw new Error(result.stderr || `${command} failed`); + return result.stdout ?? ''; +} + +async function sha256(path) { + return createHash('sha256') + .update(await readFile(path)) + .digest('hex'); +} + +async function migrationChecksum(path) { + return createHash('sha256') + .update((await readFile(path, 'utf8')).replace(/\r\n/gu, '\n').trim()) + .digest('hex'); +} diff --git a/scripts/prepublish-check.mjs b/scripts/prepublish-check.mjs index a7d552b..b79bb94 100644 --- a/scripts/prepublish-check.mjs +++ b/scripts/prepublish-check.mjs @@ -25,8 +25,8 @@ function main() { const output = `${result.stdout}${result.stderr}`; assert.match( output, - /@gnolith\/taproot 0\.4\.0 public npm provenance verified/u, - 'Taproot provenance verification did not complete before publication stopped', + /@gnolith\/taproot 0\.5\.0 candidate integrity verified/u, + 'Taproot candidate verification did not complete before publication stopped', ); assert.doesNotMatch( output, diff --git a/scripts/readiness-check.mjs b/scripts/readiness-check.mjs index 501aa12..2addaa8 100644 --- a/scripts/readiness-check.mjs +++ b/scripts/readiness-check.mjs @@ -5,7 +5,8 @@ import { join } from 'node:path'; const manifest = JSON.parse(readFileSync('package.json', 'utf8')); assert.equal(manifest.private, false); assert.notEqual(manifest.version, '0.0.0'); -assert.equal(manifest.peerDependencies['@gnolith/waystone'], '>=0.1.0 <0.3.0'); +assert.equal(manifest.peerDependencies['@gnolith/diamond'], '>=0.5.0-0 <0.6.0'); +assert.equal(manifest.peerDependencies['@gnolith/taproot'], '>=0.5.0-0 <0.6.0'); assert.equal( manifest.repository.url, 'git+https://github.com/gnolith/workshop.git', @@ -14,31 +15,31 @@ assert.equal( manifest.scripts['release:boundary-check'], 'node scripts/prepublish-check.mjs --verify-dry-run', ); -assert.equal( - manifest.scripts.check.match(/npm run release:boundary-check/gu)?.length, - 1, -); assert.equal(manifest.scripts.check.match(/npm run pack:check/gu)?.length, 1); -assert.ok( - manifest.scripts.check.indexOf('npm run pack:check') < - manifest.scripts.check.indexOf('npm run release:boundary-check'), -); +assert.equal(manifest.bin['gnolith-workshop'], './dist/cli.js'); const expectedExports = [ '.', - './core', + './backup', + './bootstrap', + './client', + './executable', + './local-adapter', './mcp', './migrations', './package.json', './protocol', + './schemas/*', './server', - './site', - './styles.css', - './ui', ]; assert.deepEqual(Object.keys(manifest.exports).sort(), expectedExports); for (const value of Object.values(manifest.exports)) { - if (typeof value === 'string') assert.ok(existsSync(value)); + if (typeof value === 'string') + assert.ok( + existsSync( + value.includes('*') ? value.slice(0, value.indexOf('*')) : value, + ), + ); else { assert.ok(existsSync(value.import)); assert.ok(existsSync(value.types)); @@ -49,59 +50,44 @@ const sourceFiles = walk('src').filter((path) => /\.tsx?$/u.test(path)); const source = sourceFiles.map((path) => readFileSync(path, 'utf8')).join('\n'); assert.doesNotMatch(source, /TODO|FIXME|no public API|coming soon/iu); -const browserFiles = [ - ...walk('src/ui').filter((path) => /\.tsx?$/u.test(path)), - ...walk('src/protocol').filter((path) => /\.ts$/u.test(path)), - 'src/ui.ts', - 'src/protocol.ts', -]; -const browserSource = browserFiles - .map((path) => readFileSync(path, 'utf8')) - .join('\n'); -assert.doesNotMatch( - browserSource, - /(?:from|import\()\s*['"][^'"]*\/(?:server|mcp|site)(?:\/|['"])/u, -); -assert.doesNotMatch(browserSource, /['"]node:/u); -assert.ok(readFileSync('src/ui.ts', 'utf8').startsWith("'use client';")); -assert.ok(browserSource.includes('createWorkshopPlugin')); - const migrationIds = readdirSync('migrations') .filter((name) => /\.sql$/u.test(name)) .map((name) => name.replace(/\.sql$/u, '')); assert.equal(new Set(migrationIds).size, migrationIds.length); assert.deepEqual(migrationIds, [...migrationIds].sort()); +assert.equal(migrationIds.at(-1), '0011_administration_control_plane'); const readme = readFileSync('README.md', 'utf8'); -const evidence = readFileSync('docs/release-evidence.md', 'utf8'); -const checklist = readFileSync('docs/release-checklist.md', 'utf8'); +const architecture = readFileSync('docs/architecture-reset.md', 'utf8'); +const breaking = readFileSync('docs/breaking-0.5.md', 'utf8'); const compatibility = readFileSync('COMPATIBILITY.md', 'utf8'); -assert.ok(readme.includes('@gnolith/workshop/site')); -assert.ok(readme.includes('histories are bounded newest-first canonical')); -assert.ok(evidence.includes('Workshop package handoff ready')); -assert.ok(evidence.includes('isolated package-runtime consumers')); -assert.ok(evidence.includes('does not qualify a')); -assert.ok(evidence.includes('complete Gnolith Site')); -assert.ok(evidence.includes('machine-verifiable record')); -assert.ok(evidence.includes('PACKAGE GATES PASS; TAGGED RELEASE EVIDENCE')); -assert.ok(evidence.includes('PENDING')); -assert.ok( - evidence.includes('Workshop CI must not provision, deploy, assemble'), +const peerArtifacts = JSON.parse( + readFileSync('docs/peer-artifacts-v1.json', 'utf8'), ); -assert.ok( - checklist.includes('public, provenance-verified `@gnolith/taproot` 0.4.0'), +assert.ok(readme.includes('exactly 52 versioned operations')); +assert.ok(architecture.includes('Schema version 11')); +assert.ok(architecture.includes('does not inspect Taproot search tables')); +assert.ok(breaking.includes('Schema 11 is required')); +assert.ok(compatibility.includes('Diamond and Taproot 0.5')); +assert.equal(peerArtifacts.format, 'gnolith-workshop-peer-artifacts-v1'); +assert.equal(peerArtifacts.version, 1); +assert.deepEqual( + peerArtifacts.peers.map(({ name }) => name), + ['diamond', 'taproot'], ); -assert.ok(checklist.includes('security, native SQLite')); -assert.ok(checklist.includes('Normal `npm publish` derives this tag')); -assert.ok(checklist.includes('tag-driven OIDC workflow')); -assert.ok(compatibility.includes('Waystone plugin contract `>=0.1.0 <0.3.0`')); -assert.ok(compatibility.includes('WorkshopCompatibleWaystonePlugin')); -assert.ok( - compatibility.includes('required\nTask/Memory history methods') || - compatibility.includes('required Task/Memory history methods'), +assert.equal( + peerArtifacts.peers[1].commit, + '98c7bc6e66b39d4bba0862a7e5dcb3a58a7882ac', +); +assert.equal( + peerArtifacts.peers[1].sha256, + '030e0bc4b58873b1ba362cc7ea0a232b769fcd5163ce7679d397c47b8f15b818', +); +assert.equal( + peerArtifacts.peers[1].canonicalEvidence.sbom.sha256, + 'cee47a765e917037ce0c3d0c28e481d0a48c204f729e9a28dff730df6bf5f3fc', ); -assert.doesNotMatch(checklist, /combined-system acceptance passes/iu); -assert.ok(existsSync('docs/release-provenance.schema.json')); +assert.ok(existsSync('schemas/workshop-config-v1.json')); console.log('repository readiness invariants passed'); function walk(root) { diff --git a/scripts/release-block.mjs b/scripts/release-block.mjs index 0a33894..f00722c 100644 --- a/scripts/release-block.mjs +++ b/scripts/release-block.mjs @@ -12,6 +12,26 @@ const predicateType = 'https://slsa.dev/provenance/v1'; const sourceCommit = '819fe054ebb867e1ca92518bfd3b1aa6c5aa277d'; const exactIntegrity = 'sha512-yYxbrUNnu74zBaxHoywGlgeG2LFz4HMzi2RLcsq83/JVEIkwbWvWZ8tuLpxFYxTAgTXG1/FHddgEJStRupe54A=='; +const candidateVersion = '0.5.0'; +const candidateIntegrity = + 'sha512-uVA0FBUyQA3SwNGtxk93mHeoZibzzddHvbmSJ8krZKWekLbHSGNDtYBCmtPTpMZPhlrWMJYYBO+7NjpFy1Tkbg=='; + +export function verifyInstalledTaprootCandidate() { + const lock = JSON.parse(readFileSync('package-lock.json', 'utf8')); + const locked = lock.packages?.['node_modules/@gnolith/taproot']; + const installed = JSON.parse( + readFileSync('node_modules/@gnolith/taproot/package.json', 'utf8'), + ); + assert.equal(installed.name, packageName); + assert.equal(installed.version, candidateVersion); + assert.equal(locked?.version, candidateVersion); + assert.equal(locked?.integrity, candidateIntegrity); + return { + packageName, + version: candidateVersion, + integrity: candidateIntegrity, + }; +} export async function verifyTaprootRelease({ fetchImpl = globalThis.fetch, @@ -176,8 +196,8 @@ if ( process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href ) { - const verified = await verifyTaprootRelease(); + const verified = verifyInstalledTaprootCandidate(); console.log( - `${verified.packageName} ${verified.version} public npm provenance verified`, + `${verified.packageName} ${verified.version} candidate integrity verified`, ); } diff --git a/scripts/verify-public-backup-fixture.mjs b/scripts/verify-public-backup-fixture.mjs new file mode 100644 index 0000000..d6ecd05 --- /dev/null +++ b/scripts/verify-public-backup-fixture.mjs @@ -0,0 +1,95 @@ +/* global fetch */ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +const [origin, ownerToken, receiptPath] = process.argv.slice(2); +if (!origin || !ownerToken || !receiptPath) { + throw new Error( + 'usage: node scripts/verify-public-backup-fixture.mjs ', + ); +} +const receipt = JSON.parse(await readFile(receiptPath, 'utf8')); +assert.equal(receipt.format, 'gnolith-workshop-public-backup-fixture-v1'); + +let requestId = 1; +const call = async (token, name, argumentsValue) => { + const response = await fetch(`${origin.replace(/\/$/u, '')}/mcp`, { + method: 'POST', + headers: { + authorization: `Bearer ${token}`, + accept: 'application/json, text/event-stream', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: requestId++, + method: 'tools/call', + params: { name, arguments: argumentsValue }, + }), + }); + assert.equal(response.status, 200, `${name}: HTTP ${response.status}`); + const body = await response.json(); + assert.equal(body.result?.isError, false, `${name}: ${JSON.stringify(body)}`); + return body.result.structuredContent; +}; + +const property = await call(ownerToken, 'get_entity', { entityId: 'P1' }); +assert.match(JSON.stringify(property), /Fixture source/u); +const item = await call(ownerToken, 'get_entity', { entityId: 'Q1' }); +const itemJson = JSON.stringify(item); +for (const expected of ['preferred', 'qualifiers', 'backup-fixture-reference']) + assert.match(itemJson, new RegExp(expected, 'u')); + +const resource = await call(ownerToken, 'get_resource', { + resourceId: 'resource:backup-fixture', +}); +assert.equal(resource.payload?.kind, 'inline-text'); +assert.equal(resource.payload?.text, 'Inline backup fixture resource payload.'); +const annotation = await call(ownerToken, 'get_annotation', { + annotationId: 'annotation:backup-fixture', +}); +assert.equal(annotation.target?.sourceId, 'resource:backup-fixture'); + +const task = await call(ownerToken, 'get_task', { taskId: receipt.taskId }); +assert.ok(task.completedAt); +assert.equal(task.result, 'Backup fixture completed.'); +assert.deepEqual( + await call(ownerToken, 'complete_task', receipt.completionInput), + task, +); +const memory = await call(ownerToken, 'get_memory', { + slug: 'backup-fixture-memory', +}); +assert.equal(memory.revision, 2); +const prompt = await call(ownerToken, 'get_prompt', { + promptId: receipt.promptId, +}); +assert.equal(prompt.revision, 2); + +const delegatedStatus = await call( + receipt.delegatedToken, + 'gnolith_status', + {}, +); +assert.equal(delegatedStatus.principalId, 'principal:backup-fixture'); +await call(receipt.delegatedToken, 'authorization_admin', { + version: 1, + action: 'effective-policy', + target: { kind: 'entity', id: 'Q1' }, +}); +const administration = await call(ownerToken, 'authorization_admin', { + version: 1, + action: 'status', +}); +assert.ok(administration.value.principals >= 2); +assert.ok(administration.value.credentials >= 2); +assert.ok(administration.value.audits >= 7); + +process.stdout.write( + `${JSON.stringify({ + format: 'gnolith-workshop-public-backup-verification-v1', + valid: true, + taskId: receipt.taskId, + promptId: receipt.promptId, + })}\n`, +); diff --git a/scripts/waystone-consumer-smoke.mjs b/scripts/waystone-consumer-smoke.mjs deleted file mode 100644 index 95ccacb..0000000 --- a/scripts/waystone-consumer-smoke.mjs +++ /dev/null @@ -1,209 +0,0 @@ -import assert from 'node:assert/strict'; -import { execFileSync } from 'node:child_process'; -import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { loadAndVerifyArtifact } from './artifact-provenance.mjs'; - -const npmCli = process.env.npm_execpath; -if (!npmCli) throw new Error('consumer:check must be run through npm'); - -const source = JSON.parse(readFileSync('package.json', 'utf8')); -const publicWorkshopVersion = process.env.WORKSHOP_PUBLIC_CONSUMER_VERSION; -const workshopConsumerSpec = process.env.WORKSHOP_CONSUMER_SPEC; -if (publicWorkshopVersion !== undefined && workshopConsumerSpec !== undefined) { - throw new Error( - 'Set only one of WORKSHOP_PUBLIC_CONSUMER_VERSION and WORKSHOP_CONSUMER_SPEC', - ); -} -if ( - publicWorkshopVersion !== undefined && - publicWorkshopVersion !== source.version -) { - throw new Error( - `Public Workshop consumer version ${publicWorkshopVersion} does not match ${source.version}`, - ); -} -const workshopSpec = - publicWorkshopVersion ?? - workshopConsumerSpec ?? - (await loadAndVerifyArtifact()).archive; -const root = mkdtempSync(join(tmpdir(), 'workshop-waystone-consumer-')); -const waystoneIntegrity = - 'sha512-2lzNmfn9KZC1c47ydV9DO90QXixb6yScEBeh8kfI4KaNj2cXrWOyvZxnjjz/WFPF+jmy7YTRSmGRxTxh42oA6g=='; - -writeFileSync( - join(root, 'package.json'), - `${JSON.stringify( - { - private: true, - type: 'module', - dependencies: { - '@gnolith/taproot': '0.4.0', - '@gnolith/workshop': workshopSpec, - '@gnolith/waystone': '0.2.0', - '@types/react': '19.2.14', - '@types/react-dom': '19.2.3', - react: '19.2.7', - 'react-dom': '19.2.7', - typescript: '5.9.3', - }, - }, - null, - 2, - )}\n`, -); - -// Intentionally use normal npm resolution: no --force or --legacy-peer-deps. -execFileSync( - process.execPath, - [ - npmCli, - 'install', - '--ignore-scripts', - '--no-audit', - '--no-fund', - '--prefix', - root, - ], - { stdio: 'inherit' }, -); - -const installedWorkshop = packageManifest('@gnolith/workshop'); -const installedWaystone = packageManifest('@gnolith/waystone'); -assert.equal(installedWorkshop.version, source.version); -assert.equal(installedWaystone.version, '0.2.0'); -assert.equal( - installedWorkshop.peerDependencies['@gnolith/waystone'], - '>=0.1.0 <0.3.0', -); -assert.equal(installedWaystone.peerDependencies.react, '>=19 <20'); - -const lock = JSON.parse(readFileSync(join(root, 'package-lock.json'), 'utf8')); -assert.equal( - lock.packages['node_modules/@gnolith/waystone'].integrity, - waystoneIntegrity, - 'Disposable consumer did not resolve the qualified public Waystone 0.2.0 artifact', -); - -writeFileSync( - join(root, 'declaration-probe.tsx'), - ` - import { createWorkshopPlugin } from '@gnolith/workshop/ui'; - import type { WaystonePlugin as WorkshopPlugin } from '@gnolith/workshop/ui'; - import { createWaystoneRegistry } from '@gnolith/waystone/plugin'; - import type { - WorkshopCompatibleWaystonePlugin, - WaystonePluginInput, - WaystoneRegistry, - } from '@gnolith/waystone/plugin'; - - const workshopPlugin: WorkshopPlugin = createWorkshopPlugin(); - const compatiblePlugin: WorkshopCompatibleWaystonePlugin = workshopPlugin; - const pluginInput: WaystonePluginInput = compatiblePlugin; - export const registry: WaystoneRegistry = createWaystoneRegistry([pluginInput]); - `, -); -writeFileSync( - join(root, 'tsconfig.json'), - `${JSON.stringify( - { - compilerOptions: { - target: 'ES2022', - module: 'ESNext', - moduleResolution: 'Bundler', - jsx: 'react-jsx', - lib: ['ES2022', 'DOM', 'DOM.Iterable'], - strict: true, - exactOptionalPropertyTypes: true, - noEmit: true, - skipLibCheck: true, - }, - include: ['declaration-probe.tsx'], - }, - null, - 2, - )}\n`, -); -execFileSync( - process.execPath, - [ - join(root, 'node_modules', 'typescript', 'bin', 'tsc'), - '-p', - 'tsconfig.json', - ], - { cwd: root, stdio: 'inherit' }, -); - -writeFileSync( - join(root, 'plugin-probe.mjs'), - ` - import assert from 'node:assert/strict'; - import { createWorkshopPlugin } from '@gnolith/workshop/ui'; - import { createWaystoneRegistry } from '@gnolith/waystone/plugin'; - - const registry = createWaystoneRegistry([createWorkshopPlugin()]); - assert.deepEqual( - registry.plugins.map(({ id, label }) => ({ id, label })), - [{ id: 'workshop', label: 'Workshop' }], - ); - assert.deepEqual( - registry.routeDescriptors.map(({ pluginId, contribution }) => ({ - pluginId, - id: contribution.id, - path: contribution.path, - requiresClient: contribution.requiresClient, - })), - [ - { - pluginId: 'workshop', - id: 'workshop-memories-route', - path: '/workshop/memories', - requiresClient: true, - }, - { - pluginId: 'workshop', - id: 'workshop-tasks-route', - path: '/workshop/tasks', - requiresClient: true, - }, - ], - ); - assert.deepEqual( - registry.entityPanels.map(({ pluginId, contribution }) => ({ - pluginId, - id: contribution.id, - label: contribution.label, - })), - [ - { - pluginId: 'workshop', - id: 'workshop-related-tasks', - label: 'Related tasks', - }, - ], - ); - const adaptedEntityPanel = registry.entityPanels[0].contribution.component({ - entity: { id: 'Q42' }, - }); - assert.equal(adaptedEntityPanel.props.entityId, 'Q42'); - `, -); -execFileSync(process.execPath, [join(root, 'plugin-probe.mjs')], { - cwd: root, - stdio: 'inherit', -}); - -console.log( - `normal npm, declarations, and plugin registration passed for ` + - `@gnolith/workshop@${source.version}, @gnolith/waystone@0.2.0, and React 19`, -); - -function packageManifest(name) { - return JSON.parse( - readFileSync( - join(root, 'node_modules', ...name.split('/'), 'package.json'), - 'utf8', - ), - ); -} diff --git a/scripts/worker-e2e.mjs b/scripts/worker-e2e.mjs deleted file mode 100644 index bff33cf..0000000 --- a/scripts/worker-e2e.mjs +++ /dev/null @@ -1,74 +0,0 @@ -import assert from 'node:assert/strict'; -import { randomUUID } from 'node:crypto'; -import { performance } from 'node:perf_hooks'; -import { Miniflare } from 'miniflare'; -import { workshopMigrations } from '../dist/migrations.js'; - -const scriptPath = '.wrangler/package-runtime-canary/worker.js'; -const miniflare = new Miniflare({ - modules: true, - scriptPath, - compatibilityDate: '2026-07-20', - bindings: { CANARY_TOKEN: 'worker-e2e-token' }, - d1Databases: { DB: `worker-e2e-${randomUUID()}` }, -}); - -try { - const db = await miniflare.getD1Database('DB'); - for (const migration of workshopMigrations) { - for (const statement of migration.sql.split(/;\s*(?:\r?\n|$)/u)) { - if (statement.trim()) await db.prepare(statement).run(); - } - } - - const call = async (id, method, params = {}) => { - const response = await miniflare.dispatchFetch( - 'https://worker.example/mcp', - { - method: 'POST', - headers: { - accept: 'application/json, text/event-stream', - authorization: 'Bearer worker-e2e-token', - 'content-type': 'application/json', - 'mcp-protocol-version': '2025-11-25', - }, - body: JSON.stringify({ jsonrpc: '2.0', id, method, params }), - }, - ); - assert.equal(response.status, 200); - return response.json(); - }; - - const coldStarted = performance.now(); - const initialized = await call(1, 'initialize', { - protocolVersion: '2025-11-25', - capabilities: {}, - clientInfo: { name: 'worker-e2e', version: '1.0.0' }, - }); - const coldRequestMs = Number((performance.now() - coldStarted).toFixed(2)); - assert.equal(initialized.result.protocolVersion, '2025-11-25'); - - const created = await call(2, 'tools/call', { - name: 'create_task', - arguments: { - description: 'Isolated package-runtime task', - prompt: 'Verify the published package in a local Worker runtime.', - }, - }); - assert.equal(created.result.isError, true); - assert.match( - created.result.structuredContent.error.message, - /search producer is not registered/u, - ); - - const listed = await call(3, 'tools/call', { - name: 'list_tasks', - arguments: {}, - }); - assert.equal(listed.result.structuredContent.items.length, 0); - console.log( - `isolated package-runtime Worker lifecycle passed (cold initialize ${coldRequestMs}ms)`, - ); -} finally { - await miniflare.dispose(); -} diff --git a/src/authorization-administration.ts b/src/authorization-administration.ts new file mode 100644 index 0000000..b64e9a0 --- /dev/null +++ b/src/authorization-administration.ts @@ -0,0 +1,904 @@ +import { + commitAuthorizationAdministrationV1, + prepareAuthorizationAdministrationV1, + registerAuthorizationAdministrationOwnerV1, + type AuthorizationAdministrationCommandV1, + type AuthorizationAdministrationOwnerAdapterV1, + type AuthorizationAdministrationOwnerIssuerV1, + type TaprootHostWriteCapability, + type TaprootOptions, +} from '@gnolith/taproot'; +import type { OperationCallContext } from './operations.js'; +import type { AuthenticatedPrincipal } from './protocol.js'; +import { WorkshopError } from './protocol/errors.js'; +import type { D1DatabaseLike } from './server/database.js'; +import { digestWorkshopOperationRequest } from './server/idempotency.js'; + +type Input = Readonly>; + +interface PendingAdministration { + input: Input; + context: OperationCallContext; + requestDigest: string; + command: AuthorizationAdministrationCommandV1; + target: { kind: string; id: string }; + auditId: string; + createdAt: string; +} + +type AuthorizationPolicyTarget = + | { kind: 'entity'; id: string } + | { kind: 'resource'; id: string } + | { kind: 'annotation'; id: string }; + +export interface WorkshopAuthorizationAdministrationV1 { + call(input: Input, context: OperationCallContext): Promise; + assertPrincipalActive(principal: AuthenticatedPrincipal): Promise; +} + +export interface WorkshopAuthorizationAdministrationOptionsV1 { + db: D1DatabaseLike; + taprootOptions: TaprootOptions; + hostAuthority: TaprootHostWriteCapability; + installationId: string; + ownerRegistrationId?: string; + clock?: () => Date; + createId?: () => string; +} + +export async function createWorkshopAuthorizationAdministrationV1( + options: WorkshopAuthorizationAdministrationOptionsV1, +): Promise { + const owner = createOwner(options.db); + const issuer = await registerAuthorizationAdministrationOwnerV1( + options.db, + options.taprootOptions, + options.hostAuthority, + { + installationId: options.installationId, + ownerRegistrationId: + options.ownerRegistrationId ?? 'workshop-authorization-admin-v1', + adapter: owner.adapter, + }, + ); + return new WorkshopAuthorizationAdministration(options, owner, issuer); +} + +class WorkshopAuthorizationAdministration implements WorkshopAuthorizationAdministrationV1 { + constructor( + private readonly options: WorkshopAuthorizationAdministrationOptionsV1, + private readonly owner: OwnerController, + private readonly issuer: AuthorizationAdministrationOwnerIssuerV1, + ) {} + + async assertPrincipalActive( + principal: AuthenticatedPrincipal, + ): Promise { + const row = await this.options.db + .prepare( + `SELECT lifecycle FROM workshop_authorization_principals + WHERE installation_id = ? AND principal_id = ?`, + ) + .bind(principal.installationId, principal.principalId) + .first<{ lifecycle: string }>(); + if (row && row.lifecycle !== 'active') throw forbidden(); + } + + async call(input: Input, context: OperationCallContext): Promise { + assertInstallation(this.options.installationId, context.principal); + const action = string(input.action); + if (action === 'status') return this.status(context); + if (action === 'effective-policy') return this.effective(input, context); + + const expectedAuthorizationRevision = integer(input.expectedPolicyRevision); + const idempotencyKey = string(input.idempotencyKey); + const requestDigest = await digestWorkshopOperationRequest({ + operation: 'authorization_admin', + input, + }); + const existing = await receipt( + this.options.db, + context.principal, + idempotencyKey, + ); + if (existing) { + if (existing.request_digest !== requestDigest) throw conflict(); + return { ...parseReceipt(existing.response_json), status: 'replayed' }; + } + const command = await commandFor(this.options.db, input, context); + const target = targetFor(input, context); + const createdAt = ( + this.options.clock ?? (() => new Date()) + )().toISOString(); + const auditId = `authorization-audit:${ + this.options.createId?.() ?? crypto.randomUUID() + }`; + const pending: PendingAdministration = { + input, + context, + requestDigest, + command, + target, + auditId, + createdAt, + }; + try { + const participant = await prepareAuthorizationAdministrationV1( + this.options.db, + this.options.taprootOptions, + this.options.hostAuthority, + { + installationId: this.options.installationId, + expectedAuthorizationRevision, + idempotencyKey, + command, + }, + authorization(context.principal), + ); + const ownerParticipant = await this.issuer.seal( + participant, + this.owner.issue(pending), + ); + const committed = await commitAuthorizationAdministrationV1( + participant, + ownerParticipant, + ); + const stored = await receipt( + this.options.db, + context.principal, + idempotencyKey, + ); + if (!stored || stored.request_digest !== requestDigest) + throw new WorkshopError( + 'internal_error', + 'Authorization owner receipt is unavailable', + 500, + ); + const output = parseReceipt(stored.response_json); + return { + ...output, + status: committed.replayed ? 'replayed' : 'committed', + policyRevision: committed.authorizationRevision, + }; + } catch (error) { + mapTaprootAdministrationError(error, action); + } + } + + private async status(context: OperationCallContext): Promise { + const [principals, credentials, bindings, audits] = await Promise.all([ + count( + this.options.db, + 'workshop_authorization_principals', + this.options.installationId, + ), + count( + this.options.db, + 'workshop_credentials', + this.options.installationId, + ), + count( + this.options.db, + 'workshop_oauth_bindings', + this.options.installationId, + ), + count( + this.options.db, + 'workshop_authorization_audit', + this.options.installationId, + ), + ]); + return { + version: 1, + action: 'status', + policyRevision: context.principal.authorizationRevision, + value: { principals, credentials, oauthBindings: bindings, audits }, + }; + } + + private async effective( + input: Input, + context: OperationCallContext, + ): Promise { + const { kind, id } = policyTarget(input.target); + const policy = + kind === 'entity' + ? await this.options.db + .prepare( + `SELECT source_revision, authorization_revision AS policy_revision, + workspace_id, owner_principal_id, visibility_json + FROM taproot_entity_authorization + WHERE installation_id = ? AND entity_id = ?`, + ) + .bind(this.options.installationId, id) + .first<{ + source_revision: number; + policy_revision: number; + workspace_id: string | null; + owner_principal_id: string; + visibility_json: string; + }>() + : kind === 'resource' || kind === 'annotation' + ? await this.options.db + .prepare( + `SELECT revision AS source_revision, + policy_revision, NULL AS workspace_id, + NULL AS owner_principal_id, visibility_json + FROM ${ + kind === 'resource' + ? 'taproot_resources' + : 'taproot_annotations' + } + WHERE installation_id = ? AND record_id = ?`, + ) + .bind(this.options.installationId, id) + .first<{ + source_revision: number; + policy_revision: number; + workspace_id: null; + owner_principal_id: null; + visibility_json: string; + }>() + : null; + if (!policy) + throw new WorkshopError( + 'not_found', + 'Authorization policy target was not found', + 404, + ); + return { + version: 1, + action: 'effective-policy', + policyRevision: context.principal.authorizationRevision, + value: { + target: { kind, id }, + sourceRevision: Number(policy.source_revision), + sourcePolicyRevision: Number(policy.policy_revision), + workspaceId: policy.workspace_id, + ownerPrincipalId: policy.owner_principal_id, + visibility: parsePolicyJson(policy.visibility_json), + principalId: context.principal.principalId, + activeWorkspaceId: context.principal.activeWorkspaceId, + workspaceIds: [...(context.principal.workspaceIds ?? [])], + capabilities: [...context.principal.capabilities], + }, + }; + } +} + +interface OwnerController { + adapter: AuthorizationAdministrationOwnerAdapterV1; + issue(payload: PendingAdministration): object; +} + +function createOwner(db: D1DatabaseLike): OwnerController { + const pending = new WeakMap(); + return { + adapter: { + kind: 'taproot-authorization-administration-owner-adapter-v1', + prepare(ownerToken) { + if (!ownerToken || typeof ownerToken !== 'object') + throw new WorkshopError( + 'forbidden', + 'Authorization administration owner token is invalid', + 403, + ); + const payload = pending.get(ownerToken); + if (!payload) + throw new WorkshopError( + 'forbidden', + 'Authorization administration owner token is invalid', + 403, + ); + pending.delete(ownerToken); + const action = string(payload.input.action); + const policyRevision = + integer(payload.input.expectedPolicyRevision) + 1; + const response = { + version: 1, + action, + status: 'committed', + policyRevision, + auditId: payload.auditId, + target: payload.target, + }; + return Promise.resolve({ + statements: [ + ...ownerMutationStatements(db, payload), + db + .prepare( + `INSERT INTO workshop_authorization_audit ( + audit_id, installation_id, actor_principal_id, action, + target_kind, target_id, authorization_revision, + details_json, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + payload.auditId, + payload.context.principal.installationId, + payload.context.principal.principalId, + action, + payload.target.kind, + payload.target.id, + policyRevision, + JSON.stringify({ version: 1, action, target: payload.target }), + payload.createdAt, + ), + db + .prepare( + `INSERT INTO workshop_authorization_admin_receipts ( + installation_id, actor_principal_id, idempotency_key, + action, request_digest, response_json, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + payload.context.principal.installationId, + payload.context.principal.principalId, + string(payload.input.idempotencyKey), + action, + payload.requestDigest, + JSON.stringify(response), + payload.createdAt, + ), + ], + receipt: { + format: 'workshop-authorization-administration-owner-receipt-v1', + action, + auditId: payload.auditId, + target: payload.target, + requestDigest: payload.requestDigest, + }, + }); + }, + }, + issue(payload) { + const token = {}; + pending.set(token, payload); + return token; + }, + }; +} + +function parsePolicyJson(value: string): unknown { + try { + return JSON.parse(value) as unknown; + } catch { + throw new WorkshopError( + 'internal_error', + 'Authorization policy record is invalid', + 500, + ); + } +} + +function ownerMutationStatements( + db: D1DatabaseLike, + payload: PendingAdministration, +) { + const action = string(payload.input.action); + const installationId = payload.context.principal.installationId; + const now = payload.createdAt; + switch (action) { + case 'principal-create': { + const principal = object(payload.input.principal); + return [ + db + .prepare( + `INSERT INTO workshop_authorization_principals ( + installation_id, principal_id, lifecycle, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?) + ON CONFLICT(installation_id, principal_id) DO UPDATE SET + lifecycle = excluded.lifecycle, updated_at = excluded.updated_at`, + ) + .bind( + installationId, + string(principal.principalId), + principal.enabled === false ? 'disabled' : 'active', + now, + now, + ), + ]; + } + case 'principal-enable': + case 'principal-disable': + case 'principal-revoke': + return [ + db + .prepare( + `INSERT INTO workshop_authorization_principals ( + installation_id, principal_id, lifecycle, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?) + ON CONFLICT(installation_id, principal_id) DO UPDATE SET + lifecycle = excluded.lifecycle, updated_at = excluded.updated_at`, + ) + .bind( + installationId, + string(payload.input.principalId), + action === 'principal-enable' + ? 'active' + : action === 'principal-disable' + ? 'disabled' + : 'revoked', + now, + now, + ), + ...(action === 'principal-enable' + ? [] + : [ + db + .prepare( + `UPDATE workshop_credentials SET disabled = 1 + WHERE installation_id = ? AND principal_id = ?`, + ) + .bind(installationId, string(payload.input.principalId)), + db + .prepare( + `UPDATE workshop_oauth_bindings + SET active = 0, updated_at = ? + WHERE installation_id = ? AND principal_id = ?`, + ) + .bind(now, installationId, string(payload.input.principalId)), + ]), + ]; + case 'workspace-membership-add': + case 'workspace-membership-remove': + return [ + db + .prepare( + `INSERT INTO workshop_workspace_memberships ( + installation_id, principal_id, workspace_id, active, updated_at + ) VALUES (?, ?, ?, ?, ?) + ON CONFLICT(installation_id, principal_id, workspace_id) + DO UPDATE SET active = excluded.active, updated_at = excluded.updated_at`, + ) + .bind( + installationId, + string(payload.input.principalId), + string(payload.input.workspaceId), + action === 'workspace-membership-add' ? 1 : 0, + now, + ), + ]; + case 'credential-register': { + const credential = object(payload.input.credential); + return [ + db + .prepare( + `INSERT INTO workshop_credentials ( + installation_id, credential_id, profile, token_digest, + principal_id, workspace_id, disabled, created_at + ) VALUES (?, ?, 'local-bearer-v1', ?, ?, ?, 0, ?) + ON CONFLICT(installation_id, credential_id) DO UPDATE SET + token_digest = excluded.token_digest, + principal_id = excluded.principal_id, + workspace_id = excluded.workspace_id, + disabled = 0`, + ) + .bind( + installationId, + string(credential.credentialId), + digest(credential.tokenDigest), + string(credential.principalId), + string(credential.workspaceId), + now, + ), + ]; + } + case 'credential-revoke': + return [ + db + .prepare( + `UPDATE workshop_credentials SET disabled = 1 + WHERE installation_id = ? AND credential_id = ?`, + ) + .bind(installationId, string(payload.input.credentialId)), + ]; + case 'oauth-bind': { + const binding = object(payload.input.binding); + return [ + db + .prepare( + `INSERT INTO workshop_oauth_bindings ( + installation_id, issuer, subject, principal_id, workspace_id, + credential_id, active, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?) + ON CONFLICT(installation_id, issuer, subject) DO UPDATE SET + principal_id = excluded.principal_id, + workspace_id = excluded.workspace_id, + credential_id = excluded.credential_id, + active = 1, updated_at = excluded.updated_at`, + ) + .bind( + installationId, + url(binding.issuer), + string(binding.subject), + string(binding.principalId), + string(binding.workspaceId), + string(binding.credentialId), + now, + now, + ), + ]; + } + case 'oauth-unbind': + return [ + db + .prepare( + `UPDATE workshop_oauth_bindings SET active = 0, updated_at = ? + WHERE installation_id = ? AND issuer = ? AND subject = ?`, + ) + .bind( + now, + installationId, + url(payload.input.issuer), + string(payload.input.subject), + ), + ]; + default: + return []; + } +} + +async function commandFor( + db: D1DatabaseLike, + input: Input, + context: OperationCallContext, +): Promise { + const action = string(input.action); + if (action === 'principal-create') { + const principal = object(input.principal); + return { + type: 'put-principal', + principalId: string(principal.principalId), + }; + } + if ( + action === 'principal-enable' || + action === 'principal-disable' || + action === 'principal-revoke' + ) + return { type: 'put-principal', principalId: string(input.principalId) }; + if ( + action === 'workspace-membership-add' || + action === 'workspace-membership-remove' + ) + return { type: 'put-workspace', workspaceId: string(input.workspaceId) }; + if (action === 'workspace-grant-add' || action === 'workspace-grant-remove') + return { + type: 'put-grant', + principalId: string(input.principalId), + workspaceId: string(input.workspaceId), + capability: string(input.capability), + granted: action === 'workspace-grant-add', + }; + if (action === 'credential-register') { + const credential = object(input.credential); + return { + type: 'put-principal', + principalId: string(credential.principalId), + }; + } + if (action === 'oauth-bind') { + const binding = object(input.binding); + return { type: 'put-principal', principalId: string(binding.principalId) }; + } + if (action === 'visibility-policy-set') { + const target = policyTarget(input.target); + const visibility = input.visibility as never; + const expectedRevision = integer(input.expectedTargetRevision); + switch (target.kind) { + case 'entity': + return { + type: 'set-entity-visibility', + entityId: string(target.id), + expectedRevision, + visibility, + }; + case 'resource': + return { + type: 'set-resource-visibility', + resourceId: string(target.id), + expectedRevision, + visibility, + }; + case 'annotation': + return { + type: 'set-annotation-visibility', + annotationId: string(target.id), + expectedRevision, + visibility, + }; + } + } + if (action === 'credential-revoke') { + const row = await db + .prepare( + `SELECT principal_id FROM workshop_credentials + WHERE installation_id = ? AND credential_id = ?`, + ) + .bind(context.principal.installationId, string(input.credentialId)) + .first<{ principal_id: string }>(); + return { + type: 'put-principal', + principalId: row?.principal_id ?? context.principal.principalId, + }; + } + if (action === 'oauth-unbind') { + const row = await db + .prepare( + `SELECT principal_id FROM workshop_oauth_bindings + WHERE installation_id = ? AND issuer = ? AND subject = ?`, + ) + .bind( + context.principal.installationId, + url(input.issuer), + string(input.subject), + ) + .first<{ principal_id: string }>(); + return { + type: 'put-principal', + principalId: row?.principal_id ?? context.principal.principalId, + }; + } + throw new WorkshopError( + 'validation_failed', + 'Authorization administration action is invalid', + 400, + ); +} + +function targetFor( + input: Input, + context: OperationCallContext, +): { kind: string; id: string } { + const action = string(input.action); + if (action === 'principal-create') { + const value = object(input.principal); + return { kind: 'principal', id: string(value.principalId) }; + } + if (action.startsWith('principal-')) + return { kind: 'principal', id: string(input.principalId) }; + if (action.startsWith('workspace-membership')) + return { + kind: 'membership', + id: `${string(input.principalId)}@${string(input.workspaceId)}`, + }; + if (action.startsWith('workspace-grant')) + return { + kind: 'grant', + id: `${string(input.principalId)}@${string(input.workspaceId)}:${string(input.capability)}`, + }; + if (action === 'credential-register') { + const value = object(input.credential); + return { kind: 'credential', id: string(value.credentialId) }; + } + if (action === 'credential-revoke') + return { kind: 'credential', id: string(input.credentialId) }; + if (action === 'oauth-bind') { + const value = object(input.binding); + return { + kind: 'oauth', + id: `${url(value.issuer)}#${string(value.subject)}`, + }; + } + if (action === 'oauth-unbind') + return { + kind: 'oauth', + id: `${url(input.issuer)}#${string(input.subject)}`, + }; + if (action === 'visibility-policy-set') { + return policyTarget(input.target); + } + return { kind: 'principal', id: context.principal.principalId }; +} + +function authorization(principal: AuthenticatedPrincipal) { + if (!principal.workspaceIds) + throw new WorkshopError( + 'dependency_unavailable', + 'Live Taproot authorization context is unavailable', + 503, + ); + return { + installationId: principal.installationId, + principalId: principal.principalId, + activeWorkspaceId: principal.activeWorkspaceId, + workspaceIds: principal.workspaceIds, + capabilities: principal.capabilities, + authorizationRevision: principal.authorizationRevision, + }; +} + +async function receipt( + db: D1DatabaseLike, + principal: AuthenticatedPrincipal, + key: string, +) { + return db + .prepare( + `SELECT request_digest, response_json + FROM workshop_authorization_admin_receipts + WHERE installation_id = ? AND actor_principal_id = ? + AND idempotency_key = ?`, + ) + .bind(principal.installationId, principal.principalId, key) + .first<{ request_digest: string; response_json: string }>(); +} + +async function count( + db: D1DatabaseLike, + table: string, + installationId: string, +): Promise { + const row = await db + .prepare(`SELECT COUNT(*) AS count FROM ${table} WHERE installation_id = ?`) + .bind(installationId) + .first<{ count: number }>(); + return Number(row?.count ?? 0); +} + +function parseReceipt(value: string): Record { + try { + const parsed: unknown = JSON.parse(value); + return object(parsed); + } catch { + throw new WorkshopError( + 'internal_error', + 'Authorization administration receipt is invalid', + 500, + ); + } +} + +function mapTaprootAdministrationError(error: unknown, action: string): never { + const candidate = error as { name?: unknown; code?: unknown }; + const name = typeof candidate?.name === 'string' ? candidate.name : ''; + const code = typeof candidate?.code === 'string' ? candidate.code : ''; + if (name === 'AuthorizationDeniedError' || code === 'AUTHORIZATION_DENIED') + throw forbidden(); + if ( + name === 'RevisionConflictError' || + code === 'REVISION_CONFLICT' || + code === 'SEARCH_SOURCE_REPLAY_CONFLICT' + ) + throw conflict(); + if ( + name === 'EntityNotFoundError' || + name === 'PropertyNotFoundError' || + code === 'ENTITY_NOT_FOUND' || + code === 'PROPERTY_NOT_FOUND' + ) + throw new WorkshopError( + 'not_found', + 'Authorization policy target was not found', + 404, + ); + if ( + action === 'visibility-policy-set' && + (name === 'InvalidAuthorizationError' || + name === 'InvalidEntityError' || + code === 'INVALID_AUTHORIZATION' || + code === 'INVALID_ENTITY') + ) + throw new WorkshopError( + 'validation_failed', + 'Visibility clauses are unsatisfiable for the administering principal', + 400, + ); + if ( + name === 'InvalidEntityError' || + name === 'InvalidStatementError' || + name === 'InvalidDatatypeError' || + name === 'TypeError' || + code === 'INVALID_ENTITY' || + code === 'INVALID_STATEMENT' || + code === 'INVALID_DATATYPE' + ) + throw new WorkshopError( + 'validation_failed', + 'Authorization policy target or visibility is invalid', + 400, + ); + if (error instanceof WorkshopError) throw error; + throw new WorkshopError( + 'internal_error', + 'Authorization administration failed', + 500, + { + taprootErrorName: + typeof candidate?.name === 'string' + ? candidate.name.slice(0, 128) + : 'unknown', + taprootErrorCode: + typeof candidate?.code === 'string' + ? candidate.code.slice(0, 128) + : 'unknown', + }, + ); +} + +function policyTarget(value: unknown): AuthorizationPolicyTarget { + const target = object(value); + const kind = string(target.kind); + const id = string(target.id); + if (kind === 'entity') { + if (!/^[PQ][1-9][0-9]*$/u.test(id)) + throw new WorkshopError( + 'validation_failed', + 'Authorization entity target is invalid', + 400, + ); + return { kind, id }; + } + if (kind === 'resource' || kind === 'annotation') return { kind, id }; + throw new WorkshopError( + 'validation_failed', + 'Authorization policy target kind is invalid', + 400, + ); +} + +function assertInstallation( + installationId: string, + principal: AuthenticatedPrincipal, +): void { + if (principal.installationId !== installationId) throw forbidden(); +} + +function object(value: unknown): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) + throw new WorkshopError('validation_failed', 'Expected an object', 400); + return value as Record; +} + +function string(value: unknown): string { + if (typeof value !== 'string' || !value || value.length > 4096) + throw new WorkshopError('validation_failed', 'Expected bounded text', 400); + return value; +} + +function integer(value: unknown): number { + if (!Number.isSafeInteger(value) || Number(value) < 1) + throw new WorkshopError( + 'validation_failed', + 'Expected a positive revision', + 400, + ); + return Number(value); +} + +function digest(value: unknown): string { + const result = string(value).toLowerCase(); + if (!/^[a-f0-9]{64}$/u.test(result)) + throw new WorkshopError( + 'validation_failed', + 'Credential digest is invalid', + 400, + ); + return result; +} + +function url(value: unknown): string { + const result = string(value); + let parsed: URL; + try { + parsed = new URL(result); + } catch { + throw new WorkshopError('validation_failed', 'URL is invalid', 400); + } + if (parsed.protocol !== 'https:' || parsed.username || parsed.password) + throw new WorkshopError('validation_failed', 'URL is invalid', 400); + return parsed.href; +} + +function forbidden(): WorkshopError { + return new WorkshopError('forbidden', 'Authorization denied', 403); +} + +function conflict(): WorkshopError { + return new WorkshopError( + 'conflict', + 'Authorization revision or idempotency key conflicts', + 409, + ); +} diff --git a/src/backup.ts b/src/backup.ts new file mode 100644 index 0000000..d0aca8c --- /dev/null +++ b/src/backup.ts @@ -0,0 +1,775 @@ +import { WorkshopError } from './protocol/errors.js'; +import type { D1DatabaseLike } from './server/database.js'; +import type { + MigrationLedgerBackup, + MigrationLedgerOwnerHandle, + MigrationLedgerSlice, +} from '@gnolith/diamond'; + +export * from './legacy-owner.js'; + +export interface WorkshopBackupDocumentV1 { + format: 'gnolith-workshop-backup-v1'; + version: 1; + createdAt: string; + installationId: string; + schemaVersion: number; + sections: { + workshop: unknown; + migrationLedgerSlice: unknown; + }; +} + +export interface WorkshopBackupInspection { + valid: boolean; + format: 'gnolith-workshop-backup-v1'; + version: 1; + installationId: string; + schemaVersion: number; + legacy: { + taskCount: number; + memoryCount: number; + promptCount: number; + onboarding: { + retained: number; + migrated: number; + deferred: number; + }; + }; +} + +export interface WorkshopBackupOwnerHandle { + readonly owner: '@gnolith/workshop'; + readonly connectionId: string; + readonly registrationFingerprint: string; + exportOwnedState(input: { + installationId: string; + signal: AbortSignal; + }): Promise; + inspectOwnedState(input: { state: unknown; signal: AbortSignal }): Promise<{ + taskCount: number; + memoryCount: number; + promptCount: number; + onboarding: { + retained: number; + migrated: number; + deferred: number; + }; + }>; + importOwnedState(input: { + installationId: string; + state: unknown; + expireTaskLeasesAt: string; + signal: AbortSignal; + }): Promise; +} + +export interface WorkshopMigrationLedgerBackupHandle { + readonly connectionId: string; + readonly registrationFingerprint: string; + exportOwnedSlice(input: { signal: AbortSignal }): Promise; + inspectOwnedSlice(input: { + state: unknown; + signal: AbortSignal; + }): Promise; + importOwnedSlice(input: { + state: unknown; + signal: AbortSignal; + }): Promise; +} + +export interface WorkshopBackupDependencies { + owner: WorkshopBackupOwnerHandle; + ledger: WorkshopMigrationLedgerBackupHandle; + schemaVersion: number; + clock?: () => Date; +} + +export interface CreateWorkshopOwnerHandlesOptionsV1 { + db: D1DatabaseLike; + installationId: string; + connectionId: string; + registrationFingerprint: string; + ledgerBackup: MigrationLedgerBackup; + ledgerOwner: MigrationLedgerOwnerHandle; +} + +export interface WorkshopOwnerHandlesV1 { + owner: WorkshopBackupOwnerHandle; + ledger: WorkshopMigrationLedgerBackupHandle; +} + +interface WorkshopOwnedStateV1 { + format: 'gnolith-workshop-owned-state-v1'; + installationId: string; + tables: Record[]>; +} + +const WORKSHOP_BACKUP_QUERIES = [ + ['workshop_onboarding_runs', 'SELECT * FROM workshop_onboarding_runs'], + ['workshop_onboarding_steps', 'SELECT * FROM workshop_onboarding_steps'], + [ + 'workshop_tasks', + `SELECT workshop_tasks.*, + CASE WHEN idempotency_key IS NULL THEN NULL + ELSE hex(idempotency_key) END + AS workshop_backup_idempotency_key_hex + FROM workshop_tasks WHERE installation_id = ?`, + ], + [ + 'workshop_task_revisions', + `SELECT revisions.* FROM workshop_task_revisions revisions + JOIN workshop_tasks tasks ON tasks.id = revisions.task_id + WHERE tasks.installation_id = ?`, + ], + [ + 'workshop_memories', + `SELECT workshop_memories.*, + CASE WHEN idempotency_key IS NULL THEN NULL + ELSE hex(idempotency_key) END + AS workshop_backup_idempotency_key_hex + FROM workshop_memories WHERE installation_id = ?`, + ], + [ + 'workshop_memory_revisions', + 'SELECT * FROM workshop_memory_revisions WHERE installation_id = ?', + ], + [ + 'workshop_prompts', + 'SELECT * FROM workshop_prompts WHERE installation_id = ?', + ], + [ + 'workshop_prompt_revisions', + 'SELECT * FROM workshop_prompt_revisions WHERE installation_id = ?', + ], + [ + 'workshop_cursor_snapshots', + 'SELECT * FROM workshop_cursor_snapshots WHERE installation_id = ?', + ], + [ + 'workshop_cursor_entries', + `SELECT entries.* FROM workshop_cursor_entries entries + JOIN workshop_cursor_snapshots snapshots ON snapshots.id = entries.snapshot_id + WHERE snapshots.installation_id = ?`, + ], + [ + 'workshop_task_lease_receipts', + 'SELECT * FROM workshop_task_lease_receipts WHERE installation_id = ?', + ], + [ + 'workshop_task_lease_events', + 'SELECT * FROM workshop_task_lease_events WHERE installation_id = ?', + ], + [ + 'workshop_credentials', + 'SELECT * FROM workshop_credentials WHERE installation_id = ?', + ], + [ + 'workshop_bootstrap_receipts', + 'SELECT * FROM workshop_bootstrap_receipts WHERE installation_id = ?', + ], + [ + 'workshop_operation_receipts', + 'SELECT * FROM workshop_operation_receipts WHERE installation_id = ?', + ], + [ + 'workshop_authorization_principals', + 'SELECT * FROM workshop_authorization_principals WHERE installation_id = ?', + ], + [ + 'workshop_workspace_memberships', + 'SELECT * FROM workshop_workspace_memberships WHERE installation_id = ?', + ], + [ + 'workshop_oauth_bindings', + 'SELECT * FROM workshop_oauth_bindings WHERE installation_id = ?', + ], + [ + 'workshop_authorization_admin_receipts', + 'SELECT * FROM workshop_authorization_admin_receipts WHERE installation_id = ?', + ], + [ + 'workshop_authorization_audit', + 'SELECT * FROM workshop_authorization_audit WHERE installation_id = ?', + ], + [ + 'workshop_semantic_configuration_state', + 'SELECT * FROM workshop_semantic_configuration_state WHERE installation_id = ?', + ], + [ + 'workshop_semantic_configuration_receipts', + 'SELECT * FROM workshop_semantic_configuration_receipts WHERE installation_id = ?', + ], +] as const; + +const SQL_IDENTIFIER = /^[a-z][a-z0-9_]*$/u; + +const RESTORE_DELETE_QUERIES = [ + `DELETE FROM workshop_onboarding_steps`, + `DELETE FROM workshop_onboarding_runs`, + `DELETE FROM workshop_cursor_entries + WHERE snapshot_id IN ( + SELECT id FROM workshop_cursor_snapshots WHERE installation_id = ? + )`, + `DELETE FROM workshop_cursor_snapshots WHERE installation_id = ?`, + `DELETE FROM workshop_task_lease_receipts WHERE installation_id = ?`, + `DELETE FROM workshop_task_lease_events WHERE installation_id = ?`, + `DELETE FROM workshop_task_revisions + WHERE task_id IN ( + SELECT id FROM workshop_tasks WHERE installation_id = ? + )`, + `DELETE FROM workshop_memory_revisions WHERE installation_id = ?`, + `DELETE FROM workshop_prompt_revisions WHERE installation_id = ?`, + `DELETE FROM workshop_operation_receipts WHERE installation_id = ?`, + `DELETE FROM workshop_bootstrap_receipts WHERE installation_id = ?`, + `DELETE FROM workshop_credentials WHERE installation_id = ?`, + `DELETE FROM workshop_authorization_admin_receipts WHERE installation_id = ?`, + `DELETE FROM workshop_authorization_audit WHERE installation_id = ?`, + `DELETE FROM workshop_oauth_bindings WHERE installation_id = ?`, + `DELETE FROM workshop_workspace_memberships WHERE installation_id = ?`, + `DELETE FROM workshop_authorization_principals WHERE installation_id = ?`, + `DELETE FROM workshop_semantic_configuration_receipts WHERE installation_id = ?`, + `DELETE FROM workshop_semantic_configuration_state WHERE installation_id = ?`, + `DELETE FROM workshop_prompts WHERE installation_id = ?`, + `DELETE FROM workshop_memories WHERE installation_id = ?`, + `DELETE FROM workshop_tasks WHERE installation_id = ?`, +] as const; + +const BACKUP_UNIQUE_KEYS: Readonly< + Record +> = { + workshop_onboarding_runs: [['key']], + workshop_onboarding_steps: [ + ['run_key', 'ordinal'], + ['run_key', 'step_key'], + ], + workshop_tasks: [['id'], ['idempotency_key']], + workshop_task_revisions: [['task_id', 'revision'], ['event_id']], + workshop_memories: [ + ['installation_id', 'slug'], + ['installation_id', 'idempotency_key'], + ], + workshop_memory_revisions: [ + ['installation_id', 'memory_id', 'revision'], + ['event_id'], + ], + workshop_prompts: [ + ['installation_id', 'id'], + ['installation_id', 'workspace_id', 'name'], + ], + workshop_prompt_revisions: [ + ['installation_id', 'prompt_id', 'revision'], + ['event_id'], + ], + workshop_cursor_snapshots: [['id']], + workshop_cursor_entries: [ + ['snapshot_id', 'ordinal'], + ['snapshot_id', 'record_id'], + ], + workshop_task_lease_receipts: [ + ['installation_id', 'principal_id', 'idempotency_key'], + ], + workshop_task_lease_events: [['event_id']], + workshop_credentials: [ + ['installation_id', 'credential_id'], + ['installation_id', 'token_digest'], + ], + workshop_bootstrap_receipts: [['installation_id', 'idempotency_key']], + workshop_operation_receipts: [ + ['installation_id', 'principal_id', 'operation', 'idempotency_key'], + ], + workshop_authorization_principals: [['installation_id', 'principal_id']], + workshop_workspace_memberships: [ + ['installation_id', 'principal_id', 'workspace_id'], + ], + workshop_oauth_bindings: [['installation_id', 'issuer', 'subject']], + workshop_authorization_admin_receipts: [ + ['installation_id', 'actor_principal_id', 'idempotency_key'], + ], + workshop_authorization_audit: [['audit_id']], + workshop_semantic_configuration_state: [['installation_id']], + workshop_semantic_configuration_receipts: [ + ['installation_id', 'actor_principal_id', 'idempotency_key'], + ], +}; + +/** + * Creates sealed Workshop-owned backup handles around one qualified database + * binding and Diamond's public namespaced-ledger capability. + */ +export function createWorkshopOwnerHandlesV1( + options: CreateWorkshopOwnerHandlesOptionsV1, +): WorkshopOwnerHandlesV1 { + validateIdentifier(options.installationId); + if ( + !validBinding(options.connectionId) || + !/^[a-f0-9]{64}$/u.test(options.registrationFingerprint) || + options.ledgerOwner.namespace !== '@gnolith/workshop' || + options.ledgerOwner.installationId !== options.installationId || + options.ledgerOwner.connectionId !== options.connectionId + ) + throw invalid('Workshop owner handle binding is invalid'); + + const owner: WorkshopBackupOwnerHandle = Object.freeze({ + owner: '@gnolith/workshop' as const, + connectionId: options.connectionId, + registrationFingerprint: options.registrationFingerprint, + async exportOwnedState( + input: Parameters[0], + ) { + const { installationId, signal } = input; + assertInstallation(options.installationId, installationId); + signal.throwIfAborted(); + const tables: Record[]> = {}; + for (const [table, sql] of WORKSHOP_BACKUP_QUERIES) { + signal.throwIfAborted(); + const statement = options.db.prepare(sql); + const result = await ( + sql.includes('?') ? statement.bind(installationId) : statement + ).all>(); + tables[table] = result.results.map((row) => + decodeBackupOpaqueText(table, row), + ); + } + return { + format: 'gnolith-workshop-owned-state-v1', + installationId, + tables, + } satisfies WorkshopOwnedStateV1; + }, + inspectOwnedState( + input: Parameters[0], + ) { + const { state, signal } = input; + signal.throwIfAborted(); + const decoded = normalizeOwnedState( + ownedState(state, options.installationId), + ); + return Promise.resolve({ + taskCount: decoded.tables.workshop_tasks?.length ?? 0, + memoryCount: decoded.tables.workshop_memories?.length ?? 0, + promptCount: decoded.tables.workshop_prompts?.length ?? 0, + onboarding: { + retained: decoded.tables.workshop_onboarding_runs?.length ?? 0, + migrated: + decoded.tables.workshop_onboarding_steps?.filter( + (row) => row.state === 'completed', + ).length ?? 0, + deferred: + decoded.tables.workshop_onboarding_steps?.filter( + (row) => row.state !== 'completed', + ).length ?? 0, + }, + }); + }, + async importOwnedState( + input: Parameters[0], + ) { + const { installationId, state, expireTaskLeasesAt, signal } = input; + assertInstallation(options.installationId, installationId); + signal.throwIfAborted(); + if (!Number.isFinite(Date.parse(expireTaskLeasesAt))) + throw invalid('Task lease expiry is invalid'); + const decoded = normalizeOwnedState(ownedState(state, installationId)); + const statements: ReturnType[] = + RESTORE_DELETE_QUERIES.map((sql) => + sql.includes('?') + ? options.db.prepare(sql).bind(installationId) + : options.db.prepare(sql), + ); + for (const [table] of WORKSHOP_BACKUP_QUERIES) { + for (const row of decoded.tables[table] ?? []) { + signal.throwIfAborted(); + const rebound = rebindRow(row, installationId); + const columns = Object.keys(rebound); + statements.push( + options.db + .prepare( + `INSERT INTO "${table}" (${columns + .map((column) => `"${column}"`) + .join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`, + ) + .bind(...columns.map((column) => rebound[column])), + ); + } + } + statements.push( + options.db + .prepare( + `UPDATE workshop_tasks SET lease_expires_at = ? + WHERE installation_id = ? AND lease_id IS NOT NULL`, + ) + .bind(expireTaskLeasesAt, installationId), + ); + await options.db.batch(statements); + }, + }); + + const ledger: WorkshopMigrationLedgerBackupHandle = Object.freeze({ + connectionId: options.connectionId, + registrationFingerprint: options.registrationFingerprint, + exportOwnedSlice: () => + options.ledgerBackup.exportNamespace(options.ledgerOwner), + async inspectOwnedSlice( + input: Parameters< + WorkshopMigrationLedgerBackupHandle['inspectOwnedSlice'] + >[0], + ) { + const { state, signal } = input; + signal.throwIfAborted(); + await options.ledgerBackup.validateNamespace( + options.ledgerOwner, + state as MigrationLedgerSlice, + ); + }, + async importOwnedSlice( + input: Parameters< + WorkshopMigrationLedgerBackupHandle['importOwnedSlice'] + >[0], + ) { + const { state, signal } = input; + signal.throwIfAborted(); + await options.ledgerBackup.restoreNamespace( + options.ledgerOwner, + state as MigrationLedgerSlice, + { mode: 'empty' }, + ); + }, + }); + return Object.freeze({ owner, ledger }); +} + +export function createWorkshopBackupApi( + dependencies: WorkshopBackupDependencies, +) { + validateBinding(dependencies); + const clock = dependencies.clock ?? (() => new Date()); + return { + async exportBackup( + installationId: string, + signal = new AbortController().signal, + ): Promise { + validateIdentifier(installationId); + const [workshop, migrationLedgerSlice] = await Promise.all([ + dependencies.owner.exportOwnedState({ installationId, signal }), + dependencies.ledger.exportOwnedSlice({ signal }), + ]); + return { + format: 'gnolith-workshop-backup-v1', + version: 1, + createdAt: clock().toISOString(), + installationId, + schemaVersion: dependencies.schemaVersion, + sections: { workshop, migrationLedgerSlice }, + }; + }, + async inspectBackup( + value: unknown, + signal = new AbortController().signal, + ): Promise { + const backup = validateBackup(value, dependencies.schemaVersion); + const legacy = await dependencies.owner.inspectOwnedState({ + state: backup.sections.workshop, + signal, + }); + await dependencies.ledger.inspectOwnedSlice({ + state: backup.sections.migrationLedgerSlice, + signal, + }); + validateCounts(legacy); + return { + valid: true, + format: backup.format, + version: backup.version, + installationId: backup.installationId, + schemaVersion: backup.schemaVersion, + legacy, + }; + }, + async importBackup( + value: unknown, + signal = new AbortController().signal, + ): Promise { + const inspected = await this.inspectBackup(value, signal); + const backup = value as WorkshopBackupDocumentV1; + await dependencies.owner.importOwnedState({ + installationId: backup.installationId, + state: backup.sections.workshop, + expireTaskLeasesAt: clock().toISOString(), + signal, + }); + await dependencies.ledger.importOwnedSlice({ + state: backup.sections.migrationLedgerSlice, + signal, + }); + return inspected; + }, + }; +} + +function validateBinding(dependencies: WorkshopBackupDependencies): void { + if ( + dependencies.owner.owner !== '@gnolith/workshop' || + !dependencies.owner.connectionId || + dependencies.owner.connectionId !== dependencies.ledger.connectionId || + !dependencies.owner.registrationFingerprint || + dependencies.owner.registrationFingerprint !== + dependencies.ledger.registrationFingerprint || + !Number.isSafeInteger(dependencies.schemaVersion) || + dependencies.schemaVersion < 1 + ) + throw invalid('Backup owner binding is invalid'); +} + +function validateBackup( + value: unknown, + schemaVersion: number, +): WorkshopBackupDocumentV1 { + if (!isRecord(value)) throw invalid('Backup document must be an object'); + exactKeys(value, [ + 'format', + 'version', + 'createdAt', + 'installationId', + 'schemaVersion', + 'sections', + ]); + if ( + value.format !== 'gnolith-workshop-backup-v1' || + value.version !== 1 || + value.schemaVersion !== schemaVersion || + typeof value.createdAt !== 'string' || + !Number.isFinite(Date.parse(value.createdAt)) || + !isRecord(value.sections) + ) + throw invalid('Backup document is incompatible'); + validateIdentifier(value.installationId); + exactKeys(value.sections, ['workshop', 'migrationLedgerSlice']); + return value as unknown as WorkshopBackupDocumentV1; +} + +function validateCounts(value: { + taskCount: number; + memoryCount: number; + promptCount: number; + onboarding: { + retained: number; + migrated: number; + deferred: number; + }; +}): void { + for (const count of [ + value.taskCount, + value.memoryCount, + value.promptCount, + value.onboarding.retained, + value.onboarding.migrated, + value.onboarding.deferred, + ]) + if (!Number.isSafeInteger(count) || count < 0) + throw invalid('Backup inspection counts are invalid'); +} + +function validateIdentifier(value: unknown): asserts value is string { + if ( + typeof value !== 'string' || + !value || + value.length > 256 || + /[\u0000-\u001f\u007f]/u.test(value) + ) + throw invalid('Backup installation identifier is invalid'); +} + +function exactKeys( + value: Record, + expected: readonly string[], +): void { + const actual = Object.keys(value).sort(); + if (JSON.stringify(actual) !== JSON.stringify([...expected].sort())) + throw invalid('Backup document has unknown or missing fields'); +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function invalid( + message: string, + details?: Readonly>, +): WorkshopError { + return new WorkshopError('validation_failed', message, 400, details); +} + +function validBinding(value: unknown): value is string { + return ( + typeof value === 'string' && + value.length > 0 && + value.length <= 256 && + !/[\u0000-\u001f\u007f]/u.test(value) + ); +} + +function assertInstallation(expected: string, actual: string): void { + validateIdentifier(actual); + if (actual !== expected) + throw invalid('Workshop owner installation binding does not match'); +} + +function ownedState( + value: unknown, + installationId: string, +): WorkshopOwnedStateV1 { + if ( + !isRecord(value) || + value.format !== 'gnolith-workshop-owned-state-v1' || + value.installationId !== installationId || + !isRecord(value.tables) + ) + throw invalid('Workshop owner state is incompatible'); + const allowed = new Set( + WORKSHOP_BACKUP_QUERIES.map(([table]) => table), + ); + if (Object.keys(value.tables).some((table) => !allowed.has(table))) + throw invalid('Workshop owner state contains an unknown table'); + for (const [table, rows] of Object.entries(value.tables)) { + if ( + !allowed.has(table) || + !Array.isArray(rows) || + rows.some( + (row) => + !isRecord(row) || + Object.keys(row).length === 0 || + Object.keys(row).some((column) => !SQL_IDENTIFIER.test(column)), + ) + ) + throw invalid('Workshop owner state contains invalid rows or columns'); + } + return value as unknown as WorkshopOwnedStateV1; +} + +function normalizeOwnedState( + state: WorkshopOwnedStateV1, +): WorkshopOwnedStateV1 { + const tables: Record[]> = {}; + for (const [table] of WORKSHOP_BACKUP_QUERIES) { + const rows = state.tables[table] ?? []; + const uniqueKeys = BACKUP_UNIQUE_KEYS[table] ?? []; + const seen = new Map(); + const normalized: Record[] = []; + for (const row of rows) { + const canonical = canonicalize(row); + const keys = uniqueKeys + .map((columns) => ({ + columns, + key: logicalKey(table, columns, row), + })) + .filter( + ( + value, + ): value is { + columns: readonly string[]; + key: string; + } => value.key !== null, + ); + const collisions = keys + .map(({ columns, key }) => ({ + canonical: seen.get(key), + columns, + })) + .filter( + ( + value, + ): value is { + canonical: string; + columns: readonly string[]; + } => value.canonical !== undefined, + ); + if (collisions.some((value) => value.canonical !== canonical)) + throw invalid( + `Workshop owner state contains inconsistent duplicate ${table} records`, + { + backupReason: 'inconsistent-logical-duplicate', + table, + logicalKeyCount: keys.length, + collidingUniqueKeys: collisions.map(({ columns }) => + columns.join('+'), + ), + }, + ); + if (collisions.length > 0) { + for (const { key } of keys) seen.set(key, canonical); + continue; + } + for (const { key } of keys) seen.set(key, canonical); + normalized.push({ ...row }); + } + tables[table] = normalized; + } + return { ...state, tables }; +} + +function decodeBackupOpaqueText( + table: string, + row: Readonly>, +): Record { + if (table !== 'workshop_tasks' && table !== 'workshop_memories') + return { ...row }; + const encoded = row.workshop_backup_idempotency_key_hex; + if ( + encoded !== null && + (typeof encoded !== 'string' || + encoded.length % 2 !== 0 || + !/^[0-9A-F]*$/u.test(encoded)) + ) + throw invalid('Workshop owner state contains invalid opaque text'); + const decoded = { ...row }; + delete decoded.workshop_backup_idempotency_key_hex; + decoded.idempotency_key = encoded === null ? null : decodeUtf8Hex(encoded); + return decoded; +} + +function decodeUtf8Hex(value: string): string { + const bytes = new Uint8Array(value.length / 2); + for (let index = 0; index < bytes.length; index += 1) + bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16); + try { + return new TextDecoder('utf-8', { fatal: true }).decode(bytes); + } catch { + throw invalid('Workshop owner state contains invalid opaque UTF-8 text'); + } +} + +function logicalKey( + table: string, + columns: readonly string[], + row: Readonly>, +): string | null { + const values = columns.map((column) => row[column]); + if (values.some((value) => value === null || value === undefined)) + return null; + return `${table}\u0000${columns.join('\u0000')}\u0000${canonicalize(values)}`; +} + +function canonicalize(value: unknown): string { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) + return `[${value.map((entry) => canonicalize(entry)).join(',')}]`; + const record = value as Record; + return `{${Object.keys(record) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalize(record[key])}`) + .join(',')}}`; +} + +function rebindRow( + row: Readonly>, + installationId: string, +): Record { + return Object.fromEntries( + Object.entries(row).map(([key, value]) => [ + key, + key === 'installation_id' ? installationId : value, + ]), + ); +} +export * from './resource-blobs.js'; diff --git a/src/bootstrap.ts b/src/bootstrap.ts new file mode 100644 index 0000000..523cd6e --- /dev/null +++ b/src/bootstrap.ts @@ -0,0 +1,351 @@ +import type { + AuthorizationBootstrapOwnerAdapterV1, + AuthorizationBootstrapOwnerIssuerV1, + AuthorizationBootstrapOwnerParticipantV1, + AuthorizationBootstrapParticipantV1, + AuthorizationBootstrapReceiptV1, +} from '@gnolith/taproot'; +import { + decodeLocalBearerCredentialFile, + digestLocalBearerToken, +} from './server/auth.js'; +import type { D1DatabaseLike } from './server/database.js'; +import { WorkshopError } from './protocol/errors.js'; + +export const WORKSHOP_BOOTSTRAP_VERSION = 1 as const; +export const WORKSHOP_OWNER_GRANTS_V1 = [ + 'admin', + 'knowledge:policy', + 'knowledge:write', + 'memory:write', + 'prompt:write', + 'read', + 'search:admin', + 'task:write', +] as const; + +export interface ProtectedCredentialSelector { + kind: 'file' | 'inherited-descriptor'; + value: string | number; +} + +export interface BootstrapWorkshopV1Input { + version: 1; + installationId: string; + baseIri: string; + principalId: string; + workspaceId: string; + credential: ProtectedCredentialSelector; + idempotencyKey: string; + profile: 'local-bearer-v1'; +} + +interface WorkshopBootstrapOwnerPayloadV1 { + installationId: string; + credentialId: string; + credentialDigest: string; + principalId: string; + workspaceId: string; + idempotencyKey: string; + grantManifestDigest: string; + createdAt: string; +} + +export interface WorkshopBootstrapOwnerControllerV1 { + readonly adapter: AuthorizationBootstrapOwnerAdapterV1; + issue(payload: WorkshopBootstrapOwnerPayloadV1): object; +} + +export function createWorkshopBootstrapOwnerAdapterV1( + db: D1DatabaseLike, +): WorkshopBootstrapOwnerControllerV1 { + const pending = new WeakMap(); + return { + adapter: { + kind: 'taproot-authorization-bootstrap-owner-adapter-v1', + prepare(ownerToken) { + if (!isObjectToken(ownerToken)) throw invalidOwnerToken(); + const payload = pending.get(ownerToken); + if (!payload) throw invalidOwnerToken(); + pending.delete(ownerToken); + const ownerReceipt = { + format: 'workshop-authorization-bootstrap-owner-receipt-v1', + installationId: payload.installationId, + credentialId: payload.credentialId, + credentialDigest: payload.credentialDigest, + principalId: payload.principalId, + workspaceId: payload.workspaceId, + idempotencyKey: payload.idempotencyKey, + grantManifestDigest: payload.grantManifestDigest, + workshopSchemaVersion: 11, + } as const; + const receiptJson = canonicalize(ownerReceipt); + return Promise.resolve({ + statements: [ + db + .prepare( + `INSERT INTO workshop_authorization_principals ( + installation_id, principal_id, lifecycle, + created_at, updated_at + ) VALUES (?, ?, 'active', ?, ?) + ON CONFLICT(installation_id, principal_id) DO NOTHING`, + ) + .bind( + payload.installationId, + payload.principalId, + payload.createdAt, + payload.createdAt, + ), + db + .prepare( + `INSERT INTO workshop_workspace_memberships ( + installation_id, principal_id, workspace_id, + active, updated_at + ) VALUES (?, ?, ?, 1, ?) + ON CONFLICT(installation_id, principal_id, workspace_id) + DO NOTHING`, + ) + .bind( + payload.installationId, + payload.principalId, + payload.workspaceId, + payload.createdAt, + ), + db + .prepare( + `INSERT INTO workshop_credentials ( + installation_id, credential_id, profile, token_digest, + principal_id, workspace_id, disabled, created_at + ) VALUES (?, ?, 'local-bearer-v1', ?, ?, ?, 0, ?) + ON CONFLICT(installation_id, credential_id) DO NOTHING`, + ) + .bind( + payload.installationId, + payload.credentialId, + payload.credentialDigest, + payload.principalId, + payload.workspaceId, + payload.createdAt, + ), + db + .prepare( + `INSERT INTO workshop_bootstrap_receipts ( + installation_id, idempotency_key, credential_id, + principal_id, workspace_id, grant_manifest_digest, + owner_receipt_json, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(installation_id, idempotency_key) DO NOTHING`, + ) + .bind( + payload.installationId, + payload.idempotencyKey, + payload.credentialId, + payload.principalId, + payload.workspaceId, + payload.grantManifestDigest, + receiptJson, + payload.createdAt, + ), + ], + receipt: ownerReceipt, + }); + }, + }, + issue(payload) { + const token = {}; + pending.set(token, payload); + return token; + }, + }; +} + +export interface BootstrapWorkshopV1Dependencies { + prepareAuthorizationBootstrapV1(input: { + installationId: string; + baseIri: string; + principalId: string; + workspaceId: string; + grants: readonly string[]; + idempotencyKey: string; + }): Promise; + ownerParticipantIssuer: AuthorizationBootstrapOwnerIssuerV1; + ownerAdapter: WorkshopBootstrapOwnerControllerV1; + commitAuthorizationBootstrapV1( + participant: AuthorizationBootstrapParticipantV1, + ownerParticipant: AuthorizationBootstrapOwnerParticipantV1, + ): Promise; + readProtectedCredential( + selector: ProtectedCredentialSelector, + ): Promise; + workshopSchemaVersion: number; + clock?: () => Date; +} + +export interface BootstrapWorkshopV1Receipt { + version: 1; + bootstrapId: string; + installationId: string; + principalId: string; + workspaceId: string; + authorizationRevision: number; + grantManifestDigest: string; + replayed: boolean; + workshopSchemaVersion: number; + credentialId: string; + credentialDigest: string; +} + +export async function bootstrapWorkshopV1( + input: BootstrapWorkshopV1Input, + dependencies: BootstrapWorkshopV1Dependencies, +): Promise { + validateInput(input); + if (dependencies.workshopSchemaVersion !== 11) + throw new WorkshopError( + 'dependency_unavailable', + 'Workshop schema migration 0009 is required before bootstrap', + 503, + ); + const credentialFile = await dependencies.readProtectedCredential( + input.credential, + ); + let tokenBytes: Uint8Array | undefined; + try { + tokenBytes = decodeLocalBearerCredentialFile(credentialFile); + const credentialDigest = await digestLocalBearerToken(tokenBytes); + const grantManifestDigest = await sha256Hex( + JSON.stringify(WORKSHOP_OWNER_GRANTS_V1), + ); + const credentialId = `credential:${( + await sha256Hex( + canonicalize({ + installationId: input.installationId, + idempotencyKey: input.idempotencyKey, + credentialDigest, + }), + ) + ).slice(0, 32)}`; + const participant = await dependencies.prepareAuthorizationBootstrapV1({ + installationId: input.installationId, + baseIri: input.baseIri, + principalId: input.principalId, + workspaceId: input.workspaceId, + grants: WORKSHOP_OWNER_GRANTS_V1, + idempotencyKey: input.idempotencyKey, + }); + const ownerToken = dependencies.ownerAdapter.issue({ + installationId: input.installationId, + credentialId, + credentialDigest, + principalId: input.principalId, + workspaceId: input.workspaceId, + idempotencyKey: input.idempotencyKey, + grantManifestDigest, + createdAt: (dependencies.clock?.() ?? new Date()).toISOString(), + }); + const ownerParticipant = await dependencies.ownerParticipantIssuer.seal( + participant, + ownerToken, + ); + const committed = await dependencies.commitAuthorizationBootstrapV1( + participant, + ownerParticipant, + ); + if ( + committed.installationId !== input.installationId || + committed.principalId !== input.principalId || + committed.workspaceId !== input.workspaceId || + committed.grantManifestSha256 !== grantManifestDigest + ) + throw new WorkshopError( + 'dependency_unavailable', + 'Taproot bootstrap receipt does not match the Workshop request', + 503, + ); + return { + version: 1, + bootstrapId: committed.bootstrapId, + installationId: committed.installationId, + principalId: committed.principalId, + workspaceId: committed.workspaceId, + authorizationRevision: committed.authorizationRevision, + grantManifestDigest, + replayed: committed.replayed, + workshopSchemaVersion: dependencies.workshopSchemaVersion, + credentialId, + credentialDigest, + }; + } finally { + credentialFile.fill(0); + tokenBytes?.fill(0); + } +} + +function validateInput(input: BootstrapWorkshopV1Input): void { + if ( + input.version !== 1 || + input.profile !== 'local-bearer-v1' || + !validId(input.installationId) || + !validId(input.principalId) || + !validId(input.workspaceId) || + !validId(input.idempotencyKey) || + !input.credential || + !['file', 'inherited-descriptor'].includes(input.credential.kind) + ) + throw new WorkshopError( + 'validation_failed', + 'Bootstrap input is invalid', + 400, + ); + const baseIri = new URL(input.baseIri); + if (!['https:', 'http:'].includes(baseIri.protocol)) + throw new WorkshopError( + 'validation_failed', + 'Bootstrap base IRI is invalid', + 400, + ); +} + +function validId(value: string): boolean { + return ( + typeof value === 'string' && + value.length >= 1 && + value.length <= 256 && + !/[\u0000-\u001f\u007f]/u.test(value) + ); +} + +function canonicalize(value: unknown): string { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) + return `[${value.map((entry) => canonicalize(entry)).join(',')}]`; + const object = value as Record; + return `{${Object.keys(object) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalize(object[key])}`) + .join(',')}}`; +} + +async function sha256Hex(value: string): Promise { + const digest = await crypto.subtle.digest( + 'SHA-256', + new TextEncoder().encode(value), + ); + return [...new Uint8Array(digest)] + .map((byte) => byte.toString(16).padStart(2, '0')) + .join(''); +} + +function isObjectToken(value: unknown): value is object { + return ( + (typeof value === 'object' && value !== null) || typeof value === 'function' + ); +} + +function invalidOwnerToken(): WorkshopError { + return new WorkshopError( + 'validation_failed', + 'Workshop bootstrap owner token is invalid or already consumed', + 400, + ); +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..086e93d --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,140 @@ +#!/usr/bin/env node +import { + assembleWorkshopExecutable, + runWorkshopBootstrap, + runWorkshopSemanticConfiguration, +} from './executable.js'; +import type { BootstrapWorkshopV1Input } from './bootstrap.js'; +import type { ConfigureWorkshopSemanticsV1Input } from './semantic-configuration.js'; +import { normalizeWorkshopError } from './protocol/errors.js'; +import { readFile } from 'node:fs/promises'; + +const command = process.argv[2]; + +try { + if (command === 'serve') { + const configPath = exactOption(process.argv.slice(3), '--config'); + const service = await assembleWorkshopExecutable(configPath); + const address = await service.start(); + process.stdout.write( + `${JSON.stringify({ event: 'gnolith-workshop.started', ...address })}\n`, + ); + let stopping = false; + const stop = async () => { + if (stopping) return; + stopping = true; + try { + await service.stop(); + process.exit(0); + } catch (error) { + const normalized = normalizeWorkshopError(error); + process.stderr.write( + `${JSON.stringify({ + error: { + code: normalized.code, + message: + normalized.code === 'internal_error' + ? 'gnolith-workshop shutdown failed' + : normalized.message, + }, + })}\n`, + ); + process.exit(1); + } + }; + process.once('SIGINT', () => void stop()); + process.once('SIGTERM', () => void stop()); + } else if (command === 'bootstrap-v1') { + const options = parseOptions(process.argv.slice(3)); + assertOnlyOptions(options, [ + '--config', + '--installation-id', + '--base-iri', + '--principal', + '--workspace', + '--credential-file', + '--idempotency-key', + ]); + const receipt = await runWorkshopBootstrap(required(options, '--config'), { + version: 1, + installationId: required(options, '--installation-id'), + baseIri: required(options, '--base-iri'), + principalId: required(options, '--principal'), + workspaceId: required(options, '--workspace'), + credential: { + kind: 'file', + value: required(options, '--credential-file'), + }, + idempotencyKey: required(options, '--idempotency-key'), + profile: 'local-bearer-v1', + } satisfies BootstrapWorkshopV1Input); + process.stdout.write(`${JSON.stringify(receipt)}\n`); + } else if (command === 'configure-semantics-v1') { + const options = parseOptions(process.argv.slice(3)); + assertOnlyOptions(options, ['--config', '--input']); + const bytes = await readFile(required(options, '--input')); + if (bytes.byteLength > 1024 * 1024) + throw new Error('Semantic configuration input is too large'); + const input = JSON.parse( + new TextDecoder().decode(bytes), + ) as ConfigureWorkshopSemanticsV1Input; + const receipt = await runWorkshopSemanticConfiguration( + required(options, '--config'), + input, + ); + process.stdout.write(`${JSON.stringify(receipt)}\n`); + } else { + throw new Error( + 'Usage: gnolith-workshop serve --config | bootstrap-v1 --config --installation-id --base-iri --principal --workspace --credential-file --idempotency-key | configure-semantics-v1 --config --input ', + ); + } +} catch (error) { + const normalized = normalizeWorkshopError(error); + process.stderr.write( + `${JSON.stringify({ + error: { + code: normalized.code, + message: + normalized.code === 'internal_error' + ? 'gnolith-workshop failed' + : normalized.message, + }, + })}\n`, + ); + process.exitCode = 1; +} + +function exactOption(args: readonly string[], name: string): string { + const parsed = parseOptions(args); + if (parsed.size !== 1) throw new Error(`Only ${name} is accepted`); + return required(parsed, name); +} + +function parseOptions(args: readonly string[]): Map { + if (args.length % 2 !== 0) throw new Error('Options require values'); + const result = new Map(); + for (let index = 0; index < args.length; index += 2) { + const name = args[index]!; + const value = args[index + 1]!; + if (!name.startsWith('--') || !value || value.includes('\u0000')) + throw new Error('Invalid command option'); + if (result.has(name)) throw new Error(`Duplicate option: ${name}`); + result.set(name, value); + } + return result; +} + +function required(options: ReadonlyMap, name: string): string { + const value = options.get(name); + if (!value) throw new Error(`Missing ${name}`); + return value; +} + +function assertOnlyOptions( + options: ReadonlyMap, + allowed: readonly string[], +): void { + const accepted = new Set(allowed); + for (const name of options.keys()) + if (!accepted.has(name)) throw new Error(`Unknown option: ${name}`); +} diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..371c7d9 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,11 @@ +export { + createWorkshopClient, + type WorkshopCallOptions, + type WorkshopClient, + type WorkshopClientOptions, +} from './protocol/client.js'; +export type { + OperationInput, + OperationName, + OperationOutput, +} from './generated/operations.js'; diff --git a/src/core.ts b/src/core.ts index da4c829..d395ae1 100644 --- a/src/core.ts +++ b/src/core.ts @@ -2,12 +2,7 @@ export { createWorkshopCore } from './server/context.js'; export type { WorkshopCore, WorkshopCoreOptions } from './server/context.js'; export type { WorkshopPersistence } from './server/database.js'; export { - createWorkshopToolDispatcher, - type WorkshopDispatchFailure, - type WorkshopDispatchResult, - type WorkshopToolCall, - type WorkshopToolDispatchContext, - type WorkshopToolDispatcher, - type WorkshopToolRuntime, -} from './mcp/dispatcher.js'; -export type { WorkshopToolDefinition } from './mcp/tools.js'; + createOperationRuntime, + type OperationRuntime, + type OperationRuntimeOptions, +} from './operations.js'; diff --git a/src/diamond-peer.d.ts b/src/diamond-peer.d.ts index b5404a2..b33e8a7 100644 --- a/src/diamond-peer.d.ts +++ b/src/diamond-peer.d.ts @@ -6,4 +6,67 @@ declare module '@gnolith/diamond' { export const applyNamespacedMigrations: WorkshopMigrationLedgerApi['applyNamespacedMigrations']; export const recordMigrationAdoption: WorkshopMigrationLedgerApi['recordMigrationAdoption']; export const checksumMigration: WorkshopMigrationLedgerApi['checksumMigration']; + + export interface MigrationLedgerSlice { + readonly format: 'diamond-migration-ledger-slice-v1'; + readonly namespace: string; + readonly entries: readonly { + readonly id: string; + readonly checksum: string; + readonly appliedAt: string; + }[]; + readonly canonicalSha256: string; + } + + export interface MigrationLedgerOwnerHandle { + readonly ownerRegistrationId: string; + readonly namespace: string; + readonly installationId: string; + readonly connectionId: string; + } + + export interface MigrationLedgerBackup { + exportNamespace( + owner: MigrationLedgerOwnerHandle, + ): Promise; + validateNamespace( + owner: MigrationLedgerOwnerHandle, + slice: MigrationLedgerSlice, + ): Promise; + restoreNamespace( + owner: MigrationLedgerOwnerHandle, + slice: MigrationLedgerSlice, + options: + { mode: 'empty' } | { mode: 'exact-adopt'; confirmExactSchema: true }, + ): Promise; + } + + export interface MigrationAssemblyAuthority { + readonly format: 'diamond-migration-assembly-authority-v1'; + } + + export function createMigrationLedgerBackupV1( + db: unknown, + assemblyAuthority: MigrationAssemblyAuthority, + ): MigrationLedgerBackup; + + export function registerMigrationLedgerOwnerV1(options: { + db: unknown; + installationId: string; + namespace: string; + migrations: readonly { id: string; statements: readonly string[] }[]; + assemblyAuthority: MigrationAssemblyAuthority; + }): Promise; + + export class D1QuadSource { + constructor(db: unknown, options?: unknown); + match( + subject?: { termType?: string; value?: string } | null, + predicate?: unknown, + object?: unknown, + graph?: unknown, + ): { + pipe(destination: unknown): unknown; + }; + } } diff --git a/src/domain-operation.ts b/src/domain-operation.ts new file mode 100644 index 0000000..f06a2c6 --- /dev/null +++ b/src/domain-operation.ts @@ -0,0 +1,285 @@ +import type { OperationName } from './generated/operations.js'; +import type { + OperationCallContext, + WorkshopDomainOperationPort, +} from './operations.js'; +import type { + MemoryFilters, + PromptFilters, + TaskFilters, + UpdateTaskInput, +} from './protocol.js'; +import { WorkshopError } from './protocol/errors.js'; +import type { AuthorizationContext } from './server/authorization.js'; +import type { WorkshopCore } from './server/context.js'; + +const WORKSHOP_OPERATION_NAMES = new Set([ + 'list_tasks', + 'search_tasks', + 'get_task', + 'get_task_packet', + 'create_task', + 'update_task', + 'archive_task', + 'claim_task', + 'release_task', + 'complete_task', + 'task_history', + 'list_memories', + 'get_memory', + 'upsert_memory', + 'delete_memory', + 'memory_history', + 'list_prompts', + 'get_prompt', + 'create_prompt', + 'update_prompt', + 'delete_prompt', + 'prompt_history', +]); + +/** + * Binds Workshop's public operation contract to its owner services. The + * caller must pass a principal that has already crossed Taproot's live + * authorization fence. + */ +export function createWorkshopDomainOperationPortV1( + core: WorkshopCore, +): WorkshopDomainOperationPort { + if (!core.prompts) + throw new WorkshopError( + 'internal_error', + 'Workshop prompt operations are not configured', + 500, + ); + const prompts = core.prompts; + return Object.freeze({ + async call( + name: OperationName, + input: Readonly>, + context: OperationCallContext, + ): Promise { + if (!WORKSHOP_OPERATION_NAMES.has(name)) + throw new WorkshopError('not_found', 'Operation not found', 404); + const authorization = liveAuthorization(context); + switch (name) { + case 'list_tasks': + return core.tasks.list(taskFilters(input), authorization); + case 'search_tasks': + return core.tasks.search(taskFilters(input), authorization); + case 'get_task': + return core.tasks.get(String(input.taskId), authorization); + case 'get_task_packet': + return core.packets.get( + String(input.taskId), + authorization, + context.signal, + ); + case 'create_task': + return core.tasks.create( + { + ...record(input.data), + idempotencyKey: String(input.idempotencyKey), + description: String(input.description), + prompt: String(input.prompt), + }, + authorization, + ); + case 'update_task': { + const update = { + ...record(input.patch), + expectedRevision: Number(input.expectedRevision), + } as unknown as UpdateTaskInput; + return input.leaseToken === undefined + ? core.tasks.update(String(input.taskId), update, authorization) + : core.tasks.updateWithLease( + String(input.taskId), + update, + input.leaseToken, + authorization, + ); + } + case 'archive_task': + return core.tasks.archive( + String(input.taskId), + { + expectedRevision: Number(input.expectedRevision), + ...(input.leaseToken === undefined + ? {} + : { leaseToken: input.leaseToken }), + }, + authorization, + ); + case 'claim_task': + return core.tasks.claimLease( + input.taskId, + input.leaseSeconds, + authorization, + ); + case 'release_task': + return core.tasks.releaseLease( + { + taskId: input.taskId, + leaseToken: input.leaseToken, + expectedRevision: input.expectedRevision, + idempotencyKey: input.idempotencyKey, + }, + authorization, + ); + case 'complete_task': + return core.tasks.completeLease( + { + taskId: input.taskId, + leaseToken: input.leaseToken, + expectedRevision: input.expectedRevision, + idempotencyKey: input.idempotencyKey, + result: input.result, + outcomeKind: input.outcomeKind, + }, + authorization, + ); + case 'task_history': + return core.tasks + .history(String(input.taskId), authorization, { + ...(input.limit === undefined + ? {} + : { limit: Number(input.limit) }), + }) + .then((items) => ({ items })); + case 'list_memories': + return core.memories.list(memoryFilters(input), authorization); + case 'get_memory': + return core.memories.get(String(input.slug), authorization); + case 'upsert_memory': + return core.memories.putIdempotent( + String(input.slug), + { + ...record(input.data), + ...(input.expectedRevision === undefined + ? {} + : { expectedRevision: Number(input.expectedRevision) }), + description: String(input.description), + content: String(input.content), + }, + authorization, + String(input.idempotencyKey), + ); + case 'delete_memory': + await core.memories.delete( + String(input.slug), + Number(input.expectedRevision), + authorization, + String(input.idempotencyKey), + ); + return { deleted: true, slug: String(input.slug) }; + case 'memory_history': + return core.memories + .history(String(input.slug), authorization, { + ...(input.limit === undefined + ? {} + : { limit: Number(input.limit) }), + }) + .then((items) => ({ items })); + case 'list_prompts': + return prompts.list(promptFilters(input), authorization); + case 'get_prompt': + return prompts.get(String(input.promptId), authorization); + case 'create_prompt': + return prompts.create( + { + ...record(input.data), + name: String(input.name), + promptText: String(input.promptText), + }, + authorization, + String(input.idempotencyKey), + ); + case 'update_prompt': + return prompts.update( + String(input.promptId), + { + ...record(input.patch), + expectedRevision: Number(input.expectedRevision), + }, + authorization, + String(input.idempotencyKey), + ); + case 'delete_prompt': + await prompts.delete( + String(input.promptId), + Number(input.expectedRevision), + authorization, + String(input.idempotencyKey), + ); + return { deleted: true, promptId: String(input.promptId) }; + case 'prompt_history': + return prompts + .history(String(input.promptId), authorization, { + ...(input.limit === undefined + ? {} + : { limit: Number(input.limit) }), + }) + .then((items) => ({ items })); + default: + throw new WorkshopError('not_found', 'Operation not found', 404); + } + }, + }); +} + +function liveAuthorization( + context: OperationCallContext, +): AuthorizationContext { + const { principal } = context; + if (!Array.isArray(principal.workspaceIds)) + throw new WorkshopError( + 'internal_error', + 'Live authorization context is unavailable', + 500, + ); + return { + installationId: principal.installationId, + principalId: principal.principalId, + activeWorkspaceId: principal.activeWorkspaceId, + workspaceIds: principal.workspaceIds, + capabilities: principal.capabilities, + authorizationRevision: principal.authorizationRevision, + }; +} + +function record(value: unknown): Readonly> { + return value && typeof value === 'object' && !Array.isArray(value) + ? (value as Readonly>) + : {}; +} + +function taskFilters(input: Readonly>): TaskFilters { + return { + ...(typeof input.cursor === 'string' ? { cursor: input.cursor } : {}), + ...(input.limit === undefined ? {} : { limit: Number(input.limit) }), + ...(input.state === undefined + ? {} + : { state: input.state as NonNullable }), + ...(typeof input.text === 'string' ? { text: input.text } : {}), + }; +} + +function memoryFilters( + input: Readonly>, +): MemoryFilters { + return { + ...(typeof input.cursor === 'string' ? { cursor: input.cursor } : {}), + ...(input.limit === undefined ? {} : { limit: Number(input.limit) }), + ...(typeof input.text === 'string' ? { text: input.text } : {}), + }; +} + +function promptFilters( + input: Readonly>, +): PromptFilters { + return { + ...(typeof input.cursor === 'string' ? { cursor: input.cursor } : {}), + ...(input.limit === undefined ? {} : { limit: Number(input.limit) }), + ...(typeof input.text === 'string' ? { text: input.text } : {}), + }; +} diff --git a/src/executable.ts b/src/executable.ts new file mode 100644 index 0000000..f64b7e9 --- /dev/null +++ b/src/executable.ts @@ -0,0 +1,175 @@ +import { readFile } from 'node:fs/promises'; +import { readFileSync } from 'node:fs'; +import { pathToFileURL } from 'node:url'; +import { + bootstrapWorkshopV1, + type BootstrapWorkshopV1Dependencies, + type BootstrapWorkshopV1Input, + type ProtectedCredentialSelector, +} from './bootstrap.js'; +import { WorkshopError } from './protocol/errors.js'; +import { + validateWorkshopServiceConfig, + type ProtectedSelectorConfig, + type WorkshopServiceConfig, +} from './server/config.js'; +import { + createWorkshopHttpService, + type WorkshopHttpService, + type WorkshopHttpServiceOptions, +} from './server/service.js'; +import { + configureWorkshopSemanticsV1, + type ConfigureWorkshopSemanticsV1Dependencies, + type ConfigureWorkshopSemanticsV1Input, +} from './semantic-configuration.js'; + +const MAX_CONFIG_BYTES = 1024 * 1024; +const MAX_PROTECTED_BYTES = 1024 * 1024; + +export interface WorkshopExecutableAdapter { + createWorkshopExecutable( + config: WorkshopServiceConfig, + protectedReader: ProtectedReader, + ): Promise< + Omit + >; + createWorkshopBootstrapDependencies?( + config: WorkshopServiceConfig, + protectedReader: ProtectedReader, + ): Promise>; + createWorkshopSemanticConfigurationDependencies?( + config: WorkshopServiceConfig, + protectedReader: ProtectedReader, + ): Promise; +} + +export type ProtectedReader = ( + selector: ProtectedSelectorConfig, +) => Promise; + +export async function loadWorkshopServiceConfig( + path: string, +): Promise { + const bytes = await readFile(path); + if (bytes.byteLength > MAX_CONFIG_BYTES) + throw invalid('Configuration file is too large'); + try { + return validateWorkshopServiceConfig( + JSON.parse(new TextDecoder().decode(bytes)), + ); + } catch (error) { + if (error instanceof WorkshopError) throw error; + throw invalid('Configuration file is not valid JSON'); + } +} + +export async function assembleWorkshopExecutable( + configPath: string, +): Promise { + const config = await loadWorkshopServiceConfig(configPath); + const adapter = await loadAdapter(config.adapterModule, configPath); + const options = await adapter.createWorkshopExecutable( + config, + readProtectedSelector, + ); + return createWorkshopHttpService({ + ...options, + listener: config.listener, + ...(config.shutdownTimeoutMs + ? { shutdownTimeoutMs: config.shutdownTimeoutMs } + : {}), + }); +} + +export async function runWorkshopBootstrap( + configPath: string, + input: BootstrapWorkshopV1Input, +): Promise { + const config = await loadWorkshopServiceConfig(configPath); + const adapter = await loadAdapter(config.adapterModule, configPath); + if (!adapter.createWorkshopBootstrapDependencies) + throw invalid('Adapter does not support bootstrap-v1'); + const dependencies = await adapter.createWorkshopBootstrapDependencies( + config, + readProtectedSelector, + ); + return bootstrapWorkshopV1(input, { + ...dependencies, + readProtectedCredential: (selector) => readProtectedSelector(selector), + }); +} + +export async function runWorkshopSemanticConfiguration( + configPath: string, + input: ConfigureWorkshopSemanticsV1Input, +): Promise { + const config = await loadWorkshopServiceConfig(configPath); + const adapter = await loadAdapter(config.adapterModule, configPath); + if (!adapter.createWorkshopSemanticConfigurationDependencies) + throw invalid('Adapter does not support configure-semantics-v1'); + const dependencies = + await adapter.createWorkshopSemanticConfigurationDependencies( + config, + readProtectedSelector, + ); + return configureWorkshopSemanticsV1(input, dependencies); +} + +export async function readProtectedSelector( + selector: ProtectedSelectorConfig | ProtectedCredentialSelector, +): Promise { + let bytes: Uint8Array; + if (selector.kind === 'file') { + bytes = await readFile(String(selector.value)); + } else if (selector.kind === 'inherited-descriptor') { + if ( + typeof selector.value !== 'number' || + !Number.isSafeInteger(selector.value) || + selector.value < 3 + ) + throw invalid('Inherited descriptor selector is invalid'); + bytes = readFileSync(selector.value); + } else { + if ( + typeof selector.value !== 'string' || + !/^[A-Z][A-Z0-9_]{0,127}$/u.test(selector.value) + ) + throw invalid('Environment selector is invalid'); + const value = process.env[selector.value]; + if (value === undefined) throw invalid('Protected value is unavailable'); + if (value.startsWith('file:')) { + const path = value.slice('file:'.length); + if (!path) throw invalid('Protected file selector is invalid'); + bytes = await readFile(path); + } else if (/^fd:[3-9][0-9]*$/u.test(value)) { + bytes = readFileSync(Number(value.slice('fd:'.length))); + } else { + throw invalid( + 'Environment selector must identify a protected file or inherited descriptor', + ); + } + } + if (bytes.byteLength > MAX_PROTECTED_BYTES) { + bytes.fill(0); + throw invalid('Protected value is too large'); + } + return bytes; +} + +async function loadAdapter( + moduleSelector: string, + configPath: string, +): Promise { + const url = moduleSelector.startsWith('file:') + ? moduleSelector + : new URL(moduleSelector, pathToFileURL(configPath)).href; + const loaded = (await import(url)) as Partial; + if (typeof loaded.createWorkshopExecutable !== 'function') + throw invalid('Adapter module does not expose the executable factory'); + return loaded as WorkshopExecutableAdapter; +} + +function invalid(message: string): WorkshopError { + return new WorkshopError('validation_failed', message, 400); +} diff --git a/src/generated/operations.ts b/src/generated/operations.ts new file mode 100644 index 0000000..dd9c4b1 --- /dev/null +++ b/src/generated/operations.ts @@ -0,0 +1,6809 @@ +/* Generated by scripts/generate-operation-contract.mjs. Do not edit. */ +export const OPERATION_SCHEMA_VERSION = 2 as const; +export const OPERATION_ROUTE_PREFIX = "/api/v1/operations" as const; +export const CATALOG_DIGEST = "af455b4d6ab3560bca9c0ab48e9db7bd3cbcfa166ace875578d88d182069071f" as const; +export const operationCatalog = [ + { + "name": "gnolith_status", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/gnolith_status", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": { + "installationId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "baseIri": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "credentialId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "activeWorkspaceId": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + { + "type": "null" + } + ] + }, + "capabilities": { + "type": "array", + "maxItems": 256, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "authorizationRevision": { + "type": "integer", + "minimum": 0, + "maximum": 1000000000 + }, + "migrationReadiness": { + "type": "object", + "properties": { + "namespace": { + "const": "@gnolith/workshop" + }, + "version": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "ready": { + "type": "boolean" + } + }, + "required": [ + "namespace", + "version", + "ready" + ], + "additionalProperties": false + }, + "compatibility": { + "type": "object", + "properties": { + "diamond": { + "type": "boolean" + }, + "taproot": { + "type": "boolean" + } + }, + "required": [ + "diamond", + "taproot" + ], + "additionalProperties": false + }, + "canonicalReady": { + "type": "boolean" + }, + "authorizationReady": { + "type": "boolean" + }, + "lexicalReady": { + "type": "boolean" + }, + "semanticState": { + "type": "object", + "properties": { + "state": { + "enum": [ + "ready", + "degraded", + "unconfigured" + ] + }, + "configured": { + "type": "boolean" + }, + "revision": { + "anyOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + { + "type": "null" + } + ] + }, + "fingerprint": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + { + "type": "null" + } + ] + }, + "ready": { + "type": "boolean" + }, + "diagnostic": { + "anyOf": [ + { + "type": "object", + "properties": { + "format": { + "const": "gnolith-workshop-semantic-diagnostic-v1" + }, + "version": { + "const": 1 + }, + "code": { + "enum": [ + "materialization-pending", + "provider-unavailable" + ] + }, + "retryable": { + "type": "boolean" + }, + "backend": { + "enum": [ + "sqlite", + "qdrant" + ] + }, + "repair": { + "const": "retry" + } + }, + "required": [ + "format", + "version", + "code", + "retryable", + "backend", + "repair" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "state", + "configured", + "revision", + "fingerprint", + "ready", + "diagnostic" + ], + "additionalProperties": false + }, + "producers": { + "type": "object", + "properties": { + "ready": { + "type": "boolean" + }, + "fingerprint": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "kinds": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "uniqueItems": true, + "items": { + "enum": [ + "task", + "memory", + "prompt" + ] + } + } + }, + "required": [ + "ready", + "fingerprint", + "kinds" + ], + "additionalProperties": false + }, + "blobReady": { + "type": "boolean" + }, + "versions": { + "type": "object", + "properties": { + "server": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "operationSchema": { + "const": 2 + } + }, + "required": [ + "server", + "operationSchema" + ], + "additionalProperties": false + }, + "operationCatalogDigest": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "installationId", + "baseIri", + "principalId", + "credentialId", + "activeWorkspaceId", + "capabilities", + "authorizationRevision", + "migrationReadiness", + "compatibility", + "canonicalReady", + "authorizationReady", + "lexicalReady", + "semanticState", + "producers", + "blobReady", + "versions", + "operationCatalogDigest" + ], + "additionalProperties": false + } + }, + { + "name": "authorization_admin", + "version": 1, + "capability": "admin", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/authorization_admin", + "inputSchema": { + "oneOf": [ + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "principal-create" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principal": { + "type": "object", + "properties": { + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "principalId" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principal" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "principal-enable" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principalId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "principal-disable" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principalId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "principal-revoke" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principalId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "workspace-membership-add" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principalId", + "workspaceId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "workspace-membership-remove" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principalId", + "workspaceId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "workspace-grant-add" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "capability": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principalId", + "workspaceId", + "capability" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "workspace-grant-remove" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "capability": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "principalId", + "workspaceId", + "capability" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "credential-register" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "credential": { + "type": "object", + "properties": { + "credentialId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "tokenDigest": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "Lowercase hexadecimal SHA-256 of UTF-8 \"gnolith-local-bearer-v1\\u0000\" followed by the 32 bytes obtained by base64url-decoding the exact 43-character bearer token." + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "credentialId", + "tokenDigest", + "principalId", + "workspaceId" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "credential" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "credential-revoke" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "credentialId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "credentialId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "oauth-bind" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "binding": { + "type": "object", + "properties": { + "issuer": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "subject": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "credentialId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "issuer", + "subject", + "principalId", + "workspaceId" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "binding" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "oauth-unbind" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "issuer": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "subject": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "issuer", + "subject" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "visibility-policy-set" + }, + "expectedPolicyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "target": { + "type": "object", + "properties": { + "kind": { + "enum": [ + "entity", + "resource", + "annotation" + ] + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "kind", + "id" + ], + "additionalProperties": false + }, + "expectedTargetRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "visibility": { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "clauses": { + "description": "Canonical CNF visibility: clauses are ANDed; atoms within each clause are ORed. The resulting scope must retain the administering principal.", + "type": "array", + "maxItems": 32, + "items": { + "type": "array", + "minItems": 1, + "maxItems": 32, + "items": { + "description": "One alternative inside a visibility clause. Atoms in the same clause are ORed.", + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "const": "public" + } + }, + "required": [ + "kind" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "principal" + }, + "principalId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "kind", + "principalId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "workspace" + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "kind", + "workspaceId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "capability" + }, + "capability": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "kind", + "capability" + ], + "additionalProperties": false + } + ] + } + } + } + }, + "required": [ + "version", + "clauses" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "action", + "expectedPolicyRevision", + "idempotencyKey", + "target", + "expectedTargetRevision", + "visibility" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "status" + }, + "target": { + "type": "object", + "properties": { + "kind": { + "enum": [ + "entity", + "resource", + "annotation" + ] + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "kind", + "id" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "action" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "const": "effective-policy" + }, + "target": { + "type": "object", + "properties": { + "kind": { + "enum": [ + "entity", + "resource", + "annotation" + ] + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "kind", + "id" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "action", + "target" + ], + "additionalProperties": false + } + ] + }, + "outputSchema": { + "oneOf": [ + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "status": { + "enum": [ + "committed", + "replayed" + ] + }, + "policyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "auditId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "target": { + "type": "object", + "properties": { + "kind": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "kind", + "id" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "action", + "status", + "policyRevision", + "auditId", + "target" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "action": { + "enum": [ + "status", + "effective-policy" + ] + }, + "policyRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000000 + }, + "value": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "version", + "action", + "policyRevision", + "value" + ], + "additionalProperties": false + } + ] + } + }, + { + "name": "search", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/search", + "inputSchema": { + "type": "object", + "properties": { + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "kinds": { + "type": "array", + "maxItems": 7, + "uniqueItems": true, + "items": { + "enum": [ + "statement", + "item", + "task", + "memory", + "prompt", + "resource", + "annotation" + ] + } + }, + "cursor": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "text" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "search_hydrate", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/search_hydrate", + "inputSchema": { + "type": "object", + "properties": { + "kind": { + "enum": [ + "statement", + "item", + "task", + "memory", + "prompt", + "resource", + "annotation" + ] + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "entityType": { + "enum": [ + "item", + "property" + ] + } + }, + "required": [ + "kind", + "id", + "revision" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "search_admin", + "version": 1, + "capability": "search:admin", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": false + }, + "route": "/api/v1/operations/search_admin", + "inputSchema": { + "oneOf": [ + { + "type": "object", + "properties": { + "action": { + "const": "configure" + }, + "options": { + "type": "object", + "properties": { + "expectedRevision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "configuration": { + "type": "object", + "properties": { + "version": { + "const": 1 + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "provider": { + "type": "object", + "properties": { + "kind": { + "enum": [ + "openai-compatible", + "ollama-compatible" + ] + }, + "endpoint": { + "type": "string", + "pattern": "^https?://", + "maxLength": 4096, + "description": "Exact embeddings POST URL: openai-compatible ends in /embeddings and ollama-compatible ends in /api/embed." + }, + "model": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "dimensions": { + "type": "integer", + "minimum": 1, + "maximum": 65536 + }, + "metric": { + "enum": [ + "cosine", + "dot", + "euclid" + ] + }, + "credentialSelector": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + { + "type": "null" + } + ] + }, + "allowPrivateEndpoint": { + "type": "boolean" + }, + "redirectPolicy": { + "const": "error" + } + }, + "required": [ + "kind", + "endpoint", + "model", + "dimensions", + "metric", + "credentialSelector", + "allowPrivateEndpoint", + "redirectPolicy" + ], + "additionalProperties": false + }, + "vector": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "const": "sqlite" + } + }, + "required": [ + "kind" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "qdrant" + }, + "endpoint": { + "type": "string", + "pattern": "^https?://", + "maxLength": 4096 + }, + "collection": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "credentialSelector": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + { + "type": "null" + } + ] + }, + "allowPrivateEndpoint": { + "type": "boolean" + }, + "redirectPolicy": { + "const": "error" + } + }, + "required": [ + "kind", + "endpoint", + "collection", + "credentialSelector", + "allowPrivateEndpoint", + "redirectPolicy" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "version", + "id", + "name", + "provider", + "vector" + ], + "additionalProperties": false + } + }, + "required": [ + "expectedRevision", + "idempotencyKey", + "configuration" + ], + "additionalProperties": false + } + }, + "required": [ + "action", + "options" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "action": { + "enum": [ + "health", + "materialize", + "retry-dead", + "rebuild", + "activate", + "semantic-select", + "semantic-reconnect", + "semantic-estimate", + "semantic-approve", + "semantic-run", + "semantic-resume", + "semantic-pause", + "semantic-stop", + "semantic-retry", + "semantic-exclude", + "semantic-retire", + "semantic-delete" + ] + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "cursor": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "options": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "action" + ], + "additionalProperties": false + } + ] + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_entity", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_entity", + "inputSchema": { + "type": "object", + "properties": { + "entityId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "entityId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "entity_history", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/entity_history", + "inputSchema": { + "type": "object", + "properties": { + "entityId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "entityId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "statement_history", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/statement_history", + "inputSchema": { + "type": "object", + "properties": { + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "statementId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_entities", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_entities", + "inputSchema": { + "type": "object", + "properties": { + "entityIds": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + } + }, + "required": [ + "entityIds" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "create_item", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/create_item", + "inputSchema": { + "type": "object", + "properties": { + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "idempotencyKey", + "authoredText", + "data" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "create_property", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/create_property", + "inputSchema": { + "type": "object", + "properties": { + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "idempotencyKey", + "authoredText", + "data" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "mutate_entity", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/mutate_entity", + "inputSchema": { + "type": "object", + "properties": { + "entityId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "mutations": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "const": "label" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "set-label" + }, + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "language", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "remove-label" + }, + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + } + }, + "required": [ + "type", + "language" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "description" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "set-description" + }, + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "language", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "remove-description" + }, + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + } + }, + "required": [ + "type", + "language" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "alias" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "add-alias" + }, + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "language", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "remove-alias" + }, + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "ordinal": { + "type": "integer", + "minimum": 0, + "maximum": 1000000 + } + }, + "required": [ + "type", + "language", + "ordinal" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "sitelink" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "set-sitelink" + }, + "site": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "value": { + "type": "object", + "properties": { + "site": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "badges": { + "type": "array", + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^Q[1-9][0-9]*$" + } + }, + "url": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "site", + "title", + "badges" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "site", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "remove-sitelink" + }, + "site": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "type", + "site" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "statement" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "add-statement" + }, + "statement": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "type": { + "const": "statement" + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "rank": { + "enum": [ + "preferred", + "normal", + "deprecated" + ] + }, + "mainsnak": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + }, + "qualifiers": { + "type": "object", + "maxProperties": 200, + "patternProperties": { + "^P[1-9][0-9]*$": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + }, + "qualifiers-order": { + "type": "array", + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + } + }, + "references": { + "type": "array", + "maxItems": 200, + "items": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "snaks": { + "type": "object", + "maxProperties": 200, + "patternProperties": { + "^P[1-9][0-9]*$": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + }, + "snaks-order": { + "type": "array", + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + } + } + }, + "required": [ + "hash", + "snaks", + "snaks-order" + ], + "additionalProperties": false + } + } + }, + "required": [ + "id", + "type", + "text", + "rank", + "mainsnak", + "qualifiers", + "qualifiers-order", + "references" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "statement" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "replace-statement" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "statement": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "type": { + "const": "statement" + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "rank": { + "enum": [ + "preferred", + "normal", + "deprecated" + ] + }, + "mainsnak": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + }, + "qualifiers": { + "type": "object", + "maxProperties": 200, + "patternProperties": { + "^P[1-9][0-9]*$": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + }, + "qualifiers-order": { + "type": "array", + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + } + }, + "references": { + "type": "array", + "maxItems": 200, + "items": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "snaks": { + "type": "object", + "maxProperties": 200, + "patternProperties": { + "^P[1-9][0-9]*$": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + }, + "snaks-order": { + "type": "array", + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + } + } + }, + "required": [ + "hash", + "snaks", + "snaks-order" + ], + "additionalProperties": false + } + } + }, + "required": [ + "id", + "type", + "text", + "rank", + "mainsnak", + "qualifiers", + "qualifiers-order", + "references" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "statementId", + "statement" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "remove-statement" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "type", + "statementId" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "rank" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "type": "object", + "properties": { + "type": { + "const": "set-statement-rank" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "rank": { + "enum": [ + "preferred", + "normal", + "deprecated" + ] + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "statementId", + "rank", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "qualifier" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "add-qualifier" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "snak": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "statementId", + "snak", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "remove-qualifier" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "ordinal": { + "type": "integer", + "minimum": 0, + "maximum": 1000000 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "statementId", + "property", + "ordinal", + "text" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "const": "reference" + }, + "authoredText": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + }, + "value": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "add-reference" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "reference": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "snaks": { + "type": "object", + "maxProperties": 200, + "patternProperties": { + "^P[1-9][0-9]*$": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + }, + "snaks-order": { + "type": "array", + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + } + } + }, + "required": [ + "hash", + "snaks", + "snaks-order" + ], + "additionalProperties": false + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "statementId", + "reference", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "replace-reference" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "reference": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "snaks": { + "type": "object", + "maxProperties": 200, + "patternProperties": { + "^P[1-9][0-9]*$": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "snaktype": { + "const": "value" + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + }, + "datavalue": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 1000000 + }, + { + "type": "object", + "properties": { + "entity-type": { + "enum": [ + "item", + "property", + "lexeme", + "form", + "sense", + "entity-schema" + ] + }, + "numeric-id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "string", + "pattern": "^(?:[PQ][1-9][0-9]*|L[1-9][0-9]*(?:-[FS][1-9][0-9]*)?|E[1-9][0-9]*)$" + } + }, + "required": [ + "entity-type", + "id" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "language": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "language", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "time": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "timezone": { + "type": "integer" + }, + "before": { + "type": "integer", + "minimum": 0 + }, + "after": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "integer", + "minimum": 0 + }, + "calendarmodel": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "time", + "timezone", + "before", + "after", + "precision", + "calendarmodel" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "amount": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "unit": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "lowerBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "upperBound": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "amount", + "unit" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "altitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "precision": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ] + }, + "globe": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "latitude", + "longitude", + "altitude", + "precision", + "globe" + ], + "additionalProperties": false + } + ] + }, + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "value", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "snaktype", + "property", + "datatype", + "datavalue" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "snaktype": { + "enum": [ + "somevalue", + "novalue" + ] + }, + "property": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "datatype": { + "enum": [ + "wikibase-item", + "wikibase-property", + "wikibase-lexeme", + "wikibase-form", + "wikibase-sense", + "entity-schema", + "string", + "external-id", + "url", + "commonsMedia", + "monolingualtext", + "time", + "quantity", + "globe-coordinate", + "math", + "musical-notation", + "geo-shape", + "tabular-data" + ] + } + }, + "required": [ + "snaktype", + "property", + "datatype" + ], + "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + }, + "snaks-order": { + "type": "array", + "maxItems": 200, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^P[1-9][0-9]*$" + } + } + }, + "required": [ + "hash", + "snaks", + "snaks-order" + ], + "additionalProperties": false + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "statementId", + "hash", + "reference", + "text" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "remove-reference" + }, + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "hash": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 16384 + } + }, + "required": [ + "type", + "statementId", + "hash", + "text" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "kind", + "authoredText", + "value" + ], + "additionalProperties": false + } + ] + } + } + }, + "required": [ + "entityId", + "expectedRevision", + "idempotencyKey", + "mutations" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "entity_revision", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/entity_revision", + "inputSchema": { + "type": "object", + "properties": { + "entityId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "entityId", + "revision" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "statement_revision", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/statement_revision", + "inputSchema": { + "type": "object", + "properties": { + "statementId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "statementId", + "revision" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "export_entity_json", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/export_entity_json", + "inputSchema": { + "type": "object", + "properties": { + "entityId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "entityId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "create_resource", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/create_resource", + "inputSchema": { + "type": "object", + "properties": { + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "idempotencyKey", + "data" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_resource", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_resource", + "inputSchema": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "resourceId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "update_resource", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/update_resource", + "inputSchema": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "patch": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "resourceId", + "expectedRevision", + "idempotencyKey", + "patch" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "delete_resource", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": true, + "idempotent": true + }, + "route": "/api/v1/operations/delete_resource", + "inputSchema": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "resourceId", + "expectedRevision", + "idempotencyKey" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "resource_history", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/resource_history", + "inputSchema": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "resourceId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "resource_revision", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/resource_revision", + "inputSchema": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "resourceId", + "revision" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "hydrate_resource", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/hydrate_resource", + "inputSchema": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "resourceId", + "revision" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "create_annotation", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/create_annotation", + "inputSchema": { + "type": "object", + "properties": { + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "idempotencyKey", + "data" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_annotation", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_annotation", + "inputSchema": { + "type": "object", + "properties": { + "annotationId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "annotationId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "update_annotation", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/update_annotation", + "inputSchema": { + "type": "object", + "properties": { + "annotationId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "patch": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "annotationId", + "expectedRevision", + "idempotencyKey", + "patch" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "delete_annotation", + "version": 1, + "capability": "knowledge:write", + "annotations": { + "readOnly": false, + "destructive": true, + "idempotent": true + }, + "route": "/api/v1/operations/delete_annotation", + "inputSchema": { + "type": "object", + "properties": { + "annotationId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "annotationId", + "expectedRevision", + "idempotencyKey" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "annotation_history", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/annotation_history", + "inputSchema": { + "type": "object", + "properties": { + "annotationId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "annotationId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "annotation_revision", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/annotation_revision", + "inputSchema": { + "type": "object", + "properties": { + "annotationId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "annotationId", + "revision" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "validate_sparql", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/validate_sparql", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "maxLength": 65536 + } + }, + "required": [ + "query" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "query_sparql", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/query_sparql", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "maxLength": 65536 + }, + "dryRun": { + "type": "boolean" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "query" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "list_tasks", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/list_tasks", + "inputSchema": { + "type": "object", + "properties": { + "cursor": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "state": { + "enum": [ + "unclaimed", + "claimed", + "completed", + "archived" + ] + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "search_tasks", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/search_tasks", + "inputSchema": { + "type": "object", + "properties": { + "text": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "cursor": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "text" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_task", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_task", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_task_packet", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_task_packet", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "create_task", + "version": 1, + "capability": "task:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/create_task", + "inputSchema": { + "type": "object", + "properties": { + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "prompt": { + "type": "string", + "minLength": 1, + "maxLength": 131072 + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "idempotencyKey", + "description", + "prompt" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "update_task", + "version": 1, + "capability": "task:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/update_task", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "leaseToken": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "patch": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "taskId", + "expectedRevision", + "patch" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "archive_task", + "version": 1, + "capability": "task:write", + "annotations": { + "readOnly": false, + "destructive": true, + "idempotent": true + }, + "route": "/api/v1/operations/archive_task", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "leaseToken": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "taskId", + "expectedRevision" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "claim_task", + "version": 1, + "capability": "task:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": false + }, + "route": "/api/v1/operations/claim_task", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "leaseSeconds": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "release_task", + "version": 1, + "capability": "task:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/release_task", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "leaseToken": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "taskId", + "leaseToken", + "expectedRevision", + "idempotencyKey" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "complete_task", + "version": 1, + "capability": "task:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/complete_task", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "leaseToken": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "result": { + "type": "string", + "minLength": 1, + "maxLength": 262144 + }, + "outcomeKind": { + "enum": [ + "success", + "negative", + "inconclusive" + ] + } + }, + "required": [ + "taskId", + "leaseToken", + "expectedRevision", + "idempotencyKey", + "result" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "task_history", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/task_history", + "inputSchema": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "list_memories", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/list_memories", + "inputSchema": { + "type": "object", + "properties": { + "cursor": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_memory", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_memory", + "inputSchema": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "slug" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "upsert_memory", + "version": 1, + "capability": "memory:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/upsert_memory", + "inputSchema": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "content": { + "type": "string", + "minLength": 1, + "maxLength": 131072 + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "slug", + "idempotencyKey", + "description", + "content" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "delete_memory", + "version": 1, + "capability": "memory:write", + "annotations": { + "readOnly": false, + "destructive": true, + "idempotent": true + }, + "route": "/api/v1/operations/delete_memory", + "inputSchema": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "slug", + "expectedRevision", + "idempotencyKey" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "memory_history", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/memory_history", + "inputSchema": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "slug" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "list_prompts", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/list_prompts", + "inputSchema": { + "type": "object", + "properties": { + "cursor": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "get_prompt", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/get_prompt", + "inputSchema": { + "type": "object", + "properties": { + "promptId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "promptId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "create_prompt", + "version": 1, + "capability": "prompt:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/create_prompt", + "inputSchema": { + "type": "object", + "properties": { + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "promptText": { + "type": "string", + "minLength": 1, + "maxLength": 131072 + }, + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "idempotencyKey", + "name", + "promptText" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "update_prompt", + "version": 1, + "capability": "prompt:write", + "annotations": { + "readOnly": false, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/update_prompt", + "inputSchema": { + "type": "object", + "properties": { + "promptId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "patch": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "promptId", + "expectedRevision", + "idempotencyKey", + "patch" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "delete_prompt", + "version": 1, + "capability": "prompt:write", + "annotations": { + "readOnly": false, + "destructive": true, + "idempotent": true + }, + "route": "/api/v1/operations/delete_prompt", + "inputSchema": { + "type": "object", + "properties": { + "promptId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "expectedRevision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "promptId", + "expectedRevision", + "idempotencyKey" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + { + "name": "prompt_history", + "version": 1, + "capability": "read", + "annotations": { + "readOnly": true, + "destructive": false, + "idempotent": true + }, + "route": "/api/v1/operations/prompt_history", + "inputSchema": { + "type": "object", + "properties": { + "promptId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + } + }, + "required": [ + "promptId" + ], + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + } +] as const; +export type OperationName = 'gnolith_status' | 'authorization_admin' | 'search' | 'search_hydrate' | 'search_admin' | 'get_entity' | 'entity_history' | 'statement_history' | 'get_entities' | 'create_item' | 'create_property' | 'mutate_entity' | 'entity_revision' | 'statement_revision' | 'export_entity_json' | 'create_resource' | 'get_resource' | 'update_resource' | 'delete_resource' | 'resource_history' | 'resource_revision' | 'hydrate_resource' | 'create_annotation' | 'get_annotation' | 'update_annotation' | 'delete_annotation' | 'annotation_history' | 'annotation_revision' | 'validate_sparql' | 'query_sparql' | 'list_tasks' | 'search_tasks' | 'get_task' | 'get_task_packet' | 'create_task' | 'update_task' | 'archive_task' | 'claim_task' | 'release_task' | 'complete_task' | 'task_history' | 'list_memories' | 'get_memory' | 'upsert_memory' | 'delete_memory' | 'memory_history' | 'list_prompts' | 'get_prompt' | 'create_prompt' | 'update_prompt' | 'delete_prompt' | 'prompt_history'; +export type OperationInput = + Readonly> & { readonly __operationName?: Name }; +export type OperationOutput = + unknown & { readonly __operationName?: Name }; +export const legacyOperationNames = { + "add_alias": "mutate_entity", + "add_reference": "mutate_entity", + "add_sitelink": "mutate_entity", + "add_statement": "mutate_entity", + "add_qualifier": "mutate_entity", + "create_entity": "create_item or create_property", + "delete_alias": "mutate_entity", + "delete_reference": "mutate_entity", + "delete_sitelink": "mutate_entity", + "delete_statement": "mutate_entity", + "delete_qualifier": "mutate_entity", + "dry_run_sparql": "query_sparql with dryRun=true", + "get_search_status": "search_admin with action=health", + "query_sparql_update": null, + "reset_abandoned_claim": null, + "search_status": "gnolith_status or search_admin with action=health", + "set_description": "mutate_entity", + "set_label": "mutate_entity", + "set_rank": "mutate_entity" +} as const; diff --git a/src/index.ts b/src/index.ts index bfc918c..aa154c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,31 @@ /** Runtime-neutral package identity. Runtime APIs use explicit subpaths. */ export const workshopPackage = { name: '@gnolith/workshop', - version: '0.4.2', - schemaVersion: 6, + version: '0.5.0', + schemaVersion: 11, } as const; export type { WorkshopCapability, WorkshopPrincipal } from './protocol.js'; +export { + configureWorkshopSemanticsV1, + readWorkshopSemanticStatusV1, + type ConfigureWorkshopSemanticsV1Dependencies, + type ConfigureWorkshopSemanticsV1Input, + type ConfigureWorkshopSemanticsV1Receipt, + type WorkshopSemanticStatusV1, +} from './semantic-configuration.js'; +export { + createWorkshopAuthorizationAdministrationV1, + type WorkshopAuthorizationAdministrationOptionsV1, + type WorkshopAuthorizationAdministrationV1, +} from './authorization-administration.js'; +export { + createOperationRuntime, + type OperationRuntime, + type OperationRuntimeOptions, +} from './operations.js'; +export { createWorkshopDomainOperationPortV1 } from './domain-operation.js'; +export { + createWorkshopTaprootOperationPortV1, + type WorkshopTaprootAssemblyOptionsV1, +} from './taproot-operation.js'; diff --git a/src/legacy-owner.ts b/src/legacy-owner.ts new file mode 100644 index 0000000..ffe453e --- /dev/null +++ b/src/legacy-owner.ts @@ -0,0 +1,509 @@ +import { + assertWorkshopLegacyV042Schema, + type WorkshopReadOnlyPersistence, +} from './migrations.js'; +import { workshopMigrations } from './migrations/manifest.js'; +import { WorkshopError } from './protocol/errors.js'; +import type { D1DatabaseLike } from './server/database.js'; + +const LEGACY_TABLES = [ + 'workshop_cursor_entries', + 'workshop_cursor_snapshots', + 'workshop_memories', + 'workshop_memory_revisions', + 'workshop_onboarding_runs', + 'workshop_onboarding_steps', + 'workshop_prompt_revisions', + 'workshop_prompts', + 'workshop_schema', + 'workshop_task_revisions', + 'workshop_tasks', +] as const; + +const LEGACY_IMPORT_ORDER: readonly Exclude< + LegacyTableName, + 'workshop_schema' +>[] = [ + 'workshop_onboarding_runs', + 'workshop_onboarding_steps', + 'workshop_tasks', + 'workshop_task_revisions', + 'workshop_memories', + 'workshop_memory_revisions', + 'workshop_prompts', + 'workshop_prompt_revisions', + 'workshop_cursor_snapshots', + 'workshop_cursor_entries', +]; + +const DEFAULT_MAX_ROWS = 50_000; +const MAX_MAX_ROWS = 100_000; +const DEFAULT_MAX_BYTES = 16 * 1024 * 1024; +const MAX_MAX_BYTES = 32 * 1024 * 1024; + +type LegacyTableName = (typeof LEGACY_TABLES)[number]; +type LegacyScalar = string | number | null; +type LegacyRow = Readonly>; + +interface LegacyOwnerPayload { + tables: Readonly>; +} + +declare const legacyOwnerFragmentBrand: unique symbol; + +/** + * Process-local, verified owner fragment. Its row representation is intentionally + * absent from the public declarations and can only be consumed by the adoption + * adapter below. + */ +export interface WorkshopLegacyOwnerFragmentV042 { + readonly [legacyOwnerFragmentBrand]: 'WorkshopLegacyOwnerFragmentV042'; +} + +export interface WorkshopLegacyV042LedgerEvidence { + namespace: '@gnolith/workshop'; + migrations: readonly { + id: string; + checksum: `sha256:${string}`; + }[]; +} + +export interface WorkshopLegacyV042Attestation { + packageName: '@gnolith/workshop'; + packageVersion: '0.4.2'; + schemaVersion: 6; + migrationLedger: WorkshopLegacyV042LedgerEvidence; +} + +export interface WorkshopLegacyV042DecodeOptions { + maxRows?: number; + maxBytes?: number; + signal?: AbortSignal; +} + +export interface WorkshopLegacyOwnerDecodedV042 { + format: 'gnolith-workshop-legacy-owner-v042'; + version: 1; + source: { + packageName: '@gnolith/workshop'; + packageVersion: '0.4.2'; + schemaVersion: 6; + }; + counts: { + tasks: number; + memories: number; + prompts: number; + revisions: number; + }; + tableCounts: Readonly>; + digests: { + tables: Readonly>; + migrationLedger: `sha256:${string}`; + ownerFragment: `sha256:${string}`; + }; + migrationLedger: WorkshopLegacyV042LedgerEvidence; + ownerFragment: WorkshopLegacyOwnerFragmentV042; +} + +export interface WorkshopLegacyOwnerAdoptionHandle { + readonly owner: '@gnolith/workshop'; + readonly connectionId: string; + readonly registrationFingerprint: string; + adoptLegacyOwnerV042(input: { + installationId: string; + fragment: WorkshopLegacyOwnerFragmentV042; + sourceDigest: `sha256:${string}`; + migrationLedger: WorkshopLegacyV042LedgerEvidence; + expireClaimsAt: string; + signal: AbortSignal; + }): Promise; +} + +export interface WorkshopLegacyOwnerDestinationBindingV1 { + connectionId: string; + registrationFingerprint: string; +} + +/** + * Creates the owner-scoped exact-0.4.2 adoption handle. The factory retains all + * Workshop table knowledge; assembly code receives only the sealed handle. + */ +export function createWorkshopLegacyOwnerAdoptionHandleV1( + db: D1DatabaseLike, + binding: WorkshopLegacyOwnerDestinationBindingV1, +): WorkshopLegacyOwnerAdoptionHandle { + validateDestinationBinding(binding); + return Object.freeze({ + owner: '@gnolith/workshop' as const, + connectionId: binding.connectionId, + registrationFingerprint: binding.registrationFingerprint, + async adoptLegacyOwnerV042( + input: Parameters< + WorkshopLegacyOwnerAdoptionHandle['adoptLegacyOwnerV042'] + >[0], + ) { + input.signal.throwIfAborted(); + const payload = fragmentPayloads.get(input.fragment); + if (!payload) throw invalid('Legacy owner fragment is not verified'); + if ((await digest(canonicalJson(payload))) !== input.sourceDigest) + throw invalid('Legacy owner fragment digest does not match'); + validateAttestation({ + packageName: '@gnolith/workshop', + packageVersion: '0.4.2', + schemaVersion: 6, + migrationLedger: input.migrationLedger, + }); + validateIdentifier(input.installationId); + if (!Number.isFinite(Date.parse(input.expireClaimsAt))) + throw invalid('Legacy claim expiry is invalid'); + + const statements: ReturnType[] = []; + for (const table of LEGACY_IMPORT_ORDER) { + for (const row of payload.tables[table]) { + input.signal.throwIfAborted(); + const rebound = rebindInstallation(row, input.installationId); + const columns = Object.keys(rebound); + statements.push( + db + .prepare( + `INSERT INTO "${table}" (${columns + .map((column) => `"${column}"`) + .join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`, + ) + .bind(...columns.map((column) => rebound[column])), + ); + } + } + // Legacy boolean claims have no v7 lease token and must never become an + // unowned live lease in the destination. + statements.push( + db + .prepare( + `UPDATE workshop_tasks + SET claimed = 0, claimed_at = NULL + WHERE installation_id = ? AND claimed != 0`, + ) + .bind(input.installationId), + ); + await db.batch(statements); + }, + }); +} + +export interface WorkshopLegacyOwnerAdoptionReceiptV1 { + format: 'gnolith-workshop-legacy-owner-adoption-v1'; + version: 1; + installationId: string; + sourceDigest: `sha256:${string}`; + adoptedAt: string; + counts: WorkshopLegacyOwnerDecodedV042['counts']; +} + +const fragmentPayloads = new WeakMap(); +const verifiedDecodes = new WeakSet(); + +/** + * Decode exact Workshop 0.4.2 state through a read-only portable DB capability. + * Only the fixed Workshop-owned allowlist above is inspected. + */ +export async function decodeWorkshopLegacyOwnerV042( + source: WorkshopReadOnlyPersistence, + attestation: WorkshopLegacyV042Attestation, + options: WorkshopLegacyV042DecodeOptions = {}, +): Promise { + validateAttestation(attestation); + const maxRows = boundedOption( + options.maxRows, + DEFAULT_MAX_ROWS, + MAX_MAX_ROWS, + 'row', + ); + const maxBytes = boundedOption( + options.maxBytes, + DEFAULT_MAX_BYTES, + MAX_MAX_BYTES, + 'byte', + ); + const signal = options.signal ?? new AbortController().signal; + signal.throwIfAborted(); + await assertWorkshopLegacyV042Schema(source); + + const tableCounts = {} as Record; + let totalRows = 0; + for (const table of LEGACY_TABLES) { + signal.throwIfAborted(); + const result = await source + .prepare(`SELECT COUNT(*) AS count FROM "${table}"`) + .first<{ count: number }>(); + const count = result?.count; + if (typeof count !== 'number' || !Number.isSafeInteger(count) || count < 0) + throw invalid(`Legacy row count for ${table} is invalid`); + tableCounts[table] = count; + totalRows += count; + if (totalRows > maxRows) + throw invalid(`Legacy owner fragment exceeds ${maxRows} rows`); + } + + const tables = {} as Record; + const tableDigests = {} as Record; + let encodedBytes = 0; + for (const table of LEGACY_TABLES) { + signal.throwIfAborted(); + const result = await source + .prepare(`SELECT * FROM "${table}" LIMIT ?`) + .bind(tableCounts[table] + 1) + .all>(); + if (result.results.length !== tableCounts[table]) + throw invalid(`Legacy source changed while reading ${table}`); + const rows = result.results + .map((row) => normalizeRow(table, row)) + .sort((left, right) => + canonicalJson(left).localeCompare(canonicalJson(right)), + ); + const encoded = canonicalJson(rows); + encodedBytes += new TextEncoder().encode(encoded).byteLength; + if (encodedBytes > maxBytes) + throw invalid(`Legacy owner fragment exceeds ${maxBytes} bytes`); + tables[table] = Object.freeze(rows); + tableDigests[table] = await digest(encoded); + } + + const migrationLedger = freezeLedger(attestation.migrationLedger); + const counts = Object.freeze({ + tasks: tableCounts.workshop_tasks, + memories: tableCounts.workshop_memories, + prompts: tableCounts.workshop_prompts, + revisions: + tableCounts.workshop_task_revisions + + tableCounts.workshop_memory_revisions + + tableCounts.workshop_prompt_revisions, + }); + const payload = Object.freeze({ + tables: Object.freeze(tables), + }) satisfies LegacyOwnerPayload; + const fragment = Object.freeze({}) as WorkshopLegacyOwnerFragmentV042; + fragmentPayloads.set(fragment, payload); + const ownerFragmentDigest = await digest(canonicalJson(payload)); + const decoded = Object.freeze({ + format: 'gnolith-workshop-legacy-owner-v042' as const, + version: 1 as const, + source: Object.freeze({ + packageName: '@gnolith/workshop' as const, + packageVersion: '0.4.2' as const, + schemaVersion: 6 as const, + }), + counts, + tableCounts: Object.freeze(tableCounts), + digests: Object.freeze({ + tables: Object.freeze(tableDigests), + migrationLedger: await digest(canonicalJson(migrationLedger)), + ownerFragment: ownerFragmentDigest, + }), + migrationLedger, + ownerFragment: fragment, + }); + verifiedDecodes.add(decoded); + return decoded; +} + +/** + * Adopt a verified fragment through Workshop's owner-scoped destination handle. + * The decoder never mutates the legacy source and this adapter exposes no CLI. + */ +export async function adoptWorkshopLegacyOwnerV042( + decoded: WorkshopLegacyOwnerDecodedV042, + destination: WorkshopLegacyOwnerAdoptionHandle, + installationId: string, + options: { clock?: () => Date; signal?: AbortSignal } = {}, +): Promise { + if ( + !verifiedDecodes.has(decoded) || + !fragmentPayloads.has(decoded.ownerFragment) + ) + throw invalid('Legacy owner decode is not verified'); + if ( + destination.owner !== '@gnolith/workshop' || + !validBindingValue(destination.connectionId) || + !/^[a-f0-9]{64}$/u.test(destination.registrationFingerprint) + ) + throw invalid('Legacy owner destination binding is invalid'); + validateIdentifier(installationId); + const signal = options.signal ?? new AbortController().signal; + signal.throwIfAborted(); + const adoptedAt = (options.clock ?? (() => new Date()))().toISOString(); + await destination.adoptLegacyOwnerV042({ + installationId, + fragment: decoded.ownerFragment, + sourceDigest: decoded.digests.ownerFragment, + migrationLedger: decoded.migrationLedger, + expireClaimsAt: adoptedAt, + signal, + }); + return { + format: 'gnolith-workshop-legacy-owner-adoption-v1', + version: 1, + installationId, + sourceDigest: decoded.digests.ownerFragment, + adoptedAt, + counts: decoded.counts, + }; +} + +function validateAttestation(attestation: WorkshopLegacyV042Attestation): void { + if (!isRecord(attestation)) + throw invalid('Legacy attestation must be an object'); + exactKeys(attestation, [ + 'packageName', + 'packageVersion', + 'schemaVersion', + 'migrationLedger', + ]); + if ( + attestation.packageName !== '@gnolith/workshop' || + attestation.packageVersion !== '0.4.2' || + attestation.schemaVersion !== 6 + ) + throw invalid('Legacy source must attest exact Workshop 0.4.2'); + if ( + !isRecord(attestation.migrationLedger) || + !Array.isArray(attestation.migrationLedger.migrations) + ) + throw invalid('Legacy migration-ledger evidence is incompatible'); + exactKeys(attestation.migrationLedger, ['namespace', 'migrations']); + const expected = workshopMigrations.slice(0, 6); + if ( + attestation.migrationLedger.namespace !== '@gnolith/workshop' || + attestation.migrationLedger.migrations.length !== expected.length + ) + throw invalid('Legacy migration-ledger evidence is incompatible'); + const migrations = attestation.migrationLedger + .migrations as readonly unknown[]; + for (let index = 0; index < expected.length; index += 1) { + const actual = migrations[index]; + const migration = expected[index]!; + if (!isRecord(actual)) + throw invalid('Legacy migration-ledger evidence is incompatible'); + exactKeys(actual, ['id', 'checksum']); + if (actual.id !== migration.id || actual.checksum !== migration.checksum) + throw invalid('Legacy migration-ledger evidence is incompatible'); + } +} + +function freezeLedger( + value: WorkshopLegacyV042LedgerEvidence, +): WorkshopLegacyV042LedgerEvidence { + return Object.freeze({ + namespace: '@gnolith/workshop', + migrations: Object.freeze( + value.migrations.map(({ id, checksum }) => + Object.freeze({ id, checksum }), + ), + ), + }); +} + +function normalizeRow( + table: LegacyTableName, + value: Record, +): LegacyRow { + const normalized: Record = {}; + for (const [key, field] of Object.entries(value).sort(([left], [right]) => + left.localeCompare(right), + )) { + if ( + field !== null && + typeof field !== 'string' && + (typeof field !== 'number' || !Number.isSafeInteger(field)) + ) + throw invalid(`Legacy ${table} contains a non-portable value`); + normalized[key] = field; + } + return Object.freeze(normalized); +} + +function canonicalJson(value: unknown): string { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) + return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`; + return `{${Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`) + .join(',')}}`; +} + +async function digest(value: string): Promise<`sha256:${string}`> { + const bytes = await crypto.subtle.digest( + 'SHA-256', + new TextEncoder().encode(value), + ); + return `sha256:${[...new Uint8Array(bytes)] + .map((byte) => byte.toString(16).padStart(2, '0')) + .join('')}`; +} + +function boundedOption( + value: number | undefined, + fallback: number, + maximum: number, + label: string, +): number { + const result = value ?? fallback; + if (!Number.isSafeInteger(result) || result < 1 || result > maximum) + throw invalid(`Legacy ${label} bound is invalid`); + return result; +} + +function exactKeys(value: object, expected: readonly string[]): void { + const actual = Object.keys(value).sort(); + if (JSON.stringify(actual) !== JSON.stringify([...expected].sort())) + throw invalid('Legacy attestation has unknown or missing fields'); +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function validateIdentifier(value: unknown): asserts value is string { + if ( + typeof value !== 'string' || + !value || + value.length > 256 || + /[\u0000-\u001f\u007f]/u.test(value) + ) + throw invalid('Legacy adoption installation identifier is invalid'); +} + +function validBindingValue(value: unknown): value is string { + return ( + typeof value === 'string' && + value.length > 0 && + value.length <= 256 && + !/[\u0000-\u001f\u007f]/u.test(value) + ); +} + +function validateDestinationBinding( + binding: WorkshopLegacyOwnerDestinationBindingV1, +): void { + if ( + !validBindingValue(binding.connectionId) || + !/^[a-f0-9]{64}$/u.test(binding.registrationFingerprint) + ) + throw invalid('Legacy owner destination binding is invalid'); +} + +function rebindInstallation( + row: LegacyRow, + installationId: string, +): Record { + return Object.fromEntries( + Object.entries(row).map(([key, value]) => [ + key, + key === 'installation_id' ? installationId : value, + ]), + ); +} + +function invalid(message: string): WorkshopError { + return new WorkshopError('validation_failed', message, 400); +} diff --git a/src/local-adapter.ts b/src/local-adapter.ts new file mode 100644 index 0000000..f38888a --- /dev/null +++ b/src/local-adapter.ts @@ -0,0 +1,148 @@ +import type { BootstrapWorkshopV1Dependencies } from './bootstrap.js'; +import { createWorkshopDomainOperationPortV1 } from './domain-operation.js'; +import type { + ProtectedReader, + WorkshopExecutableAdapter, +} from './executable.js'; +import { createOperationRuntime } from './operations.js'; +import type { WorkshopOwnerHandlesV1 } from './backup.js'; +import { WorkshopError } from './protocol/errors.js'; +import type { AuthenticationVerifier } from './server/auth.js'; +import type { VerifiedWaystoneAssets } from './server/assets.js'; +import type { WorkshopServiceConfig } from './server/config.js'; +import { + createWorkshopCore, + type WorkshopCoreOptions, +} from './server/context.js'; +import type { BoundedTriggerController } from './server/lifecycle.js'; +import type { CloseableDependency } from './server/service.js'; +import type { + VerifiedWorkshopProducerBinding, + WorkshopProducerBindingExpectation, +} from './producers.js'; +import { + createWorkshopTaprootOperationPortV1, + type WorkshopTaprootAssemblyOptionsV1, +} from './taproot-operation.js'; +import type { + DiamondSparqlPort, + WorkshopStatusSnapshot, +} from './operations.js'; +import type { ConfigureWorkshopSemanticsV1Dependencies } from './semantic-configuration.js'; + +export interface WorkshopLocalExecutableDependenciesV1 { + kind: 'workshop-local-executable-dependencies-v1'; + version: string; + workshop: WorkshopCoreOptions; + taproot: WorkshopTaprootAssemblyOptionsV1; + diamond: DiamondSparqlPort; + authentication: ( + config: WorkshopServiceConfig, + protectedReader: ProtectedReader, + ) => Promise; + producerBinding: { + binding: VerifiedWorkshopProducerBinding; + expectation: WorkshopProducerBindingExpectation; + }; + preflight: (signal: AbortSignal) => Promise; + semanticConfiguration: ConfigureWorkshopSemanticsV1Dependencies; + assets?: ( + config: WorkshopServiceConfig, + protectedReader: ProtectedReader, + ) => Promise; + trigger?: BoundedTriggerController; + dependencies?: readonly CloseableDependency[]; + ownerHandles: WorkshopOwnerHandlesV1; + bootstrap?: Omit; +} + +export interface WorkshopLocalExecutableAdapterV1 extends WorkshopExecutableAdapter { + readonly kind: 'workshop-local-executable-adapter-v1'; + readonly ownerHandles: WorkshopOwnerHandlesV1; +} + +/** + * Sole Workshop-owned production assembly. Environment adapters may qualify + * paths, secrets, and provider clients, but cannot alter catalog or dispatch. + */ +export async function createWorkshopLocalExecutableAdapterV1( + dependencies: WorkshopLocalExecutableDependenciesV1, +): Promise { + validateDependencies(dependencies); + const core = createWorkshopCore(dependencies.workshop); + const taproot = await createWorkshopTaprootOperationPortV1( + dependencies.taproot, + ); + const operations = createOperationRuntime({ + version: dependencies.version, + taproot, + diamond: dependencies.diamond, + workshop: createWorkshopDomainOperationPortV1(core), + }); + const adapter: WorkshopLocalExecutableAdapterV1 = { + kind: 'workshop-local-executable-adapter-v1' as const, + ownerHandles: dependencies.ownerHandles, + async createWorkshopExecutable(config, protectedReader) { + const authentication = await dependencies.authentication( + config, + protectedReader, + ); + const assets = await dependencies.assets?.(config, protectedReader); + return { + version: dependencies.version, + authentication, + operations, + producerBinding: dependencies.producerBinding, + preflight: dependencies.preflight, + ...(config.corsOrigins ? { corsOrigins: [...config.corsOrigins] } : {}), + ...(assets ? { assets } : {}), + ...(dependencies.trigger ? { trigger: dependencies.trigger } : {}), + dependencies: [ + ...(dependencies.dependencies ?? []), + ...(dependencies.diamond.close + ? [{ close: () => dependencies.diamond.close?.() }] + : []), + ], + }; + }, + ...(dependencies.bootstrap + ? { + createWorkshopBootstrapDependencies: () => + Promise.resolve(dependencies.bootstrap!), + } + : {}), + createWorkshopSemanticConfigurationDependencies: () => + Promise.resolve(dependencies.semanticConfiguration), + }; + return Object.freeze(adapter); +} + +export { + createWorkshopAuthorizationAuthorityV1, + createWorkshopCursorCodecV1, + createWorkshopDiamondSparqlPortV1, + createWorkshopLocalExecutableAdapterFromPrimitivesV1, + createWorkshopLocalExecutableDependenciesV1, + type WorkshopCursorSecretsV1, + type WorkshopLocalAssemblyPrimitivesV1, +} from './local-dependencies.js'; + +function validateDependencies( + dependencies: WorkshopLocalExecutableDependenciesV1, +): void { + if ( + dependencies.kind !== 'workshop-local-executable-dependencies-v1' || + !dependencies.version || + dependencies.workshop.persistence !== dependencies.taproot.db || + typeof dependencies.authentication !== 'function' || + typeof dependencies.preflight !== 'function' || + !dependencies.workshop.search || + !dependencies.ownerHandles?.owner || + !dependencies.ownerHandles?.ledger + ) + throw new WorkshopError( + 'dependency_unavailable', + 'Workshop local executable dependencies are incomplete or incompatible', + 503, + ); +} diff --git a/src/local-dependencies.ts b/src/local-dependencies.ts new file mode 100644 index 0000000..4d378b8 --- /dev/null +++ b/src/local-dependencies.ts @@ -0,0 +1,870 @@ +import { Buffer } from 'node:buffer'; +import { Readable, Transform, type TransformCallback } from 'node:stream'; +import { + createAuthorizedTaproot, + commitAuthorizationBootstrapV1, + createInstallationAuthorizationGuard, + createTaprootWorkshopPortV1, + prepareAuthorizationBootstrapV1, + registerAuthorizationBootstrapOwnerV1, + type AuthorizationContext as TaprootAuthorizationContext, + type AuthorizationCursorCodec, + type InstallationAuthorizationGuard, + type PortableResourcePayloadStoreV1, + type TaprootHostWriteCapability, + type TaprootWriteOptions, +} from '@gnolith/taproot'; +import type { + SearchMaterializationAdminGuardV1, + SearchMaterializationRunOptionsV1, + SemanticSearchAdminV1, +} from '@gnolith/taproot/search'; +import { + D1QuadSource, + createMigrationLedgerBackupV1, + registerMigrationLedgerOwnerV1, + type MigrationAssemblyAuthority, +} from '@gnolith/diamond'; +import { + createSparqlExecutor, + type SparqlExecutorOptions, +} from '@gnolith/diamond/sparql'; +import type * as RDF from '@rdfjs/types'; +import { + createWorkshopOwnerHandlesV1, + type WorkshopOwnerHandlesV1, +} from './backup.js'; +import { createWorkshopBootstrapOwnerAdapterV1 } from './bootstrap.js'; +import type { ProtectedReader } from './executable.js'; +import type { + DiamondSparqlPort, + WorkshopStatusSnapshot, +} from './operations.js'; +import { workshopNamespacedMigrationsV1 } from './migrations.js'; +import { + bindWorkshopProducerHandles, + type WorkshopProducerBindingExpectation, +} from './producers.js'; +import { WorkshopError } from './protocol/errors.js'; +import { + createAuthenticationVerifier, + createWorkshopLocalBearerCredentialStoreV1, +} from './server/auth.js'; +import type { VerifiedWaystoneAssets } from './server/assets.js'; +import type { WorkshopServiceConfig } from './server/config.js'; +import type { WorkshopCoreOptions } from './server/context.js'; +import type { + AuthorizationContext, + InstallationAuthorizationState, + WorkshopAuthorizationAuthority, +} from './server/authorization.js'; +import type { WorkshopCursorCodec } from './server/cursor.js'; +import type { + D1DatabaseLike, + D1PreparedStatementLike, + D1ResultLike, +} from './server/database.js'; +import { readWorkshopSemanticStatusV1 } from './semantic-configuration.js'; +import type { CloseableDependency } from './server/service.js'; +import { createBoundedTrigger } from './server/lifecycle.js'; +import { createWorkshopSearchIntegrationV1 } from './server/search.js'; +import { + createWorkshopLocalExecutableAdapterV1, + type WorkshopLocalExecutableAdapterV1, + type WorkshopLocalExecutableDependenciesV1, +} from './local-adapter.js'; + +export interface WorkshopCursorSecretsV1 { + generation: string; + encryptionKey: CryptoKey; + digestKey: CryptoKey; +} + +export interface WorkshopLocalAssemblyPrimitivesV1 { + kind: 'workshop-local-assembly-primitives-v1'; + version: string; + db: D1DatabaseLike; + installationId: string; + registrationFingerprint: string; + taproot: { + options: TaprootWriteOptions; + hostAuthority: TaprootHostWriteCapability; + authorizationCursorCodec: AuthorizationCursorCodec; + registrationPrincipal: { + principalId: string; + activeWorkspaceId: string | null; + }; + payloadStore?: PortableResourcePayloadStoreV1; + maxHydratedResourceBytes?: number; + }; + diamond?: Omit; + cursor: WorkshopCursorSecretsV1; + migrationAssemblyAuthority: MigrationAssemblyAuthority; + assets?: ( + config: WorkshopServiceConfig, + protectedReader: ProtectedReader, + ) => Promise; + dependencies?: readonly CloseableDependency[]; + semantic?: { + fetch?: typeof fetch; + resolveCredential?: ( + selector: string, + ) => string | Promise | undefined; + }; + workshop?: Omit< + WorkshopCoreOptions, + | 'persistence' + | 'authorization' + | 'knowledge' + | 'diamondHealth' + | 'cursorCodec' + | 'search' + >; +} + +/** + * Workshop-owned assembly boundary. The host supplies only opened databases, + * protected keys, and public Diamond/Taproot capabilities. + */ +export async function createWorkshopLocalExecutableDependenciesV1( + input: WorkshopLocalAssemblyPrimitivesV1, +): Promise { + validateInput(input); + const ownerHandles = await createLocalOwnerHandles(input); + return assembleExecutableDependencies(input, ownerHandles); +} + +/** + * Pristine-installation adapter. Bootstrap capabilities are available before + * authorization exists; serve-time runtime dependencies are assembled lazily. + */ +export async function createWorkshopLocalExecutableAdapterFromPrimitivesV1( + input: WorkshopLocalAssemblyPrimitivesV1, +): Promise { + validateInput(input); + const ownerHandles = await createLocalOwnerHandles(input); + return Object.freeze({ + kind: 'workshop-local-executable-adapter-v1' as const, + ownerHandles, + async createWorkshopExecutable( + config: WorkshopServiceConfig, + protectedReader: ProtectedReader, + ) { + const dependencies = await assembleExecutableDependencies( + input, + ownerHandles, + ); + const adapter = + await createWorkshopLocalExecutableAdapterV1(dependencies); + return adapter.createWorkshopExecutable(config, protectedReader); + }, + async createWorkshopBootstrapDependencies() { + const ownerAdapter = createWorkshopBootstrapOwnerAdapterV1(input.db); + const ownerParticipantIssuer = + await registerAuthorizationBootstrapOwnerV1( + input.db, + input.taproot.options, + input.taproot.hostAuthority, + { + installationId: input.installationId, + ownerRegistrationId: 'workshop', + adapter: ownerAdapter.adapter, + }, + ); + return { + prepareAuthorizationBootstrapV1: ( + bootstrapInput: Parameters[3], + ) => + prepareAuthorizationBootstrapV1( + input.db, + input.taproot.options, + input.taproot.hostAuthority, + bootstrapInput, + ), + ownerParticipantIssuer, + ownerAdapter, + commitAuthorizationBootstrapV1, + workshopSchemaVersion: 11, + }; + }, + async createWorkshopSemanticConfigurationDependencies() { + const dependencies = await assembleExecutableDependencies( + input, + ownerHandles, + ); + return dependencies.semanticConfiguration; + }, + }); +} + +async function assembleExecutableDependencies( + input: WorkshopLocalAssemblyPrimitivesV1, + ownerHandles: WorkshopOwnerHandlesV1, +): Promise { + const guard = await createInstallationAuthorizationGuard( + input.db, + input.taproot.options, + input.taproot.hostAuthority, + ); + const authorization = createWorkshopAuthorizationAuthorityV1(input.db, guard); + const cursorCodec = createWorkshopCursorCodecV1(input.cursor); + const operator = await createTaprootWorkshopPortV1( + input.db, + input.taproot.options, + input.taproot.hostAuthority, + { + installationId: input.installationId, + ...(input.taproot.payloadStore + ? { payloadStore: input.taproot.payloadStore } + : {}), + ...(input.taproot.maxHydratedResourceBytes === undefined + ? {} + : { maxHydratedResourceBytes: input.taproot.maxHydratedResourceBytes }), + }, + ); + const registrationContext = await operator.resolveAuthorization( + input.taproot.registrationPrincipal, + ); + const search = await createWorkshopSearchIntegrationV1({ + db: input.db, + taproot: input.taproot.options, + hostCapability: input.taproot.hostAuthority, + installationId: input.installationId, + registrationContext, + ...(input.semantic ? { semantic: input.semantic } : {}), + }); + const semanticConfiguration = { + db: input.db, + installationId: input.installationId, + semantic: search.semantic, + authorization: registrationContext, + }; + const materialization = serializeMaterializationRuns(search.materialization); + const expectation: WorkshopProducerBindingExpectation = { + installationId: input.installationId, + connectionId: ownerHandles.owner.connectionId, + registrationFingerprint: input.registrationFingerprint, + }; + const producerBinding = bindWorkshopProducerHandles(expectation, { + task: producerHandle(input, expectation, search.task), + memory: producerHandle(input, expectation, search.memory), + prompt: producerHandle(input, expectation, search.prompt), + }); + const diamond = createWorkshopDiamondSparqlPortV1(input.db, input.diamond); + const status = async () => { + const liveRegistrationContext = await operator.resolveAuthorization( + input.taproot.registrationPrincipal, + ); + return workshopLocalStatus( + input.db, + guard, + input, + expectation, + input.taproot.payloadStore !== undefined, + () => search.materialization.health(liveRegistrationContext), + () => search.semantic.status(liveRegistrationContext), + ); + }; + const workshop: WorkshopCoreOptions = { + ...(input.workshop ?? {}), + persistence: input.db, + authorization, + cursorCodec, + search, + diamondHealth: () => Promise.resolve(true), + knowledge: { + authorizedReader: (context) => + createAuthorizedTaproot( + input.db, + input.taproot.options, + toTaprootContext(context), + { cursorCodec: input.taproot.authorizationCursorCodec }, + ), + health: async () => { + await guard.readCurrentState(); + return true; + }, + }, + }; + const dependencies: WorkshopLocalExecutableDependenciesV1 = { + kind: 'workshop-local-executable-dependencies-v1' as const, + version: input.version, + workshop, + taproot: { + kind: 'workshop-taproot-assembly-v1' as const, + db: input.db, + installationId: input.installationId, + taprootOptions: input.taproot.options, + hostAuthority: input.taproot.hostAuthority, + cursorCodec: input.taproot.authorizationCursorCodec, + search: search.service, + materialization, + semantic: search.semantic, + ...(input.taproot.payloadStore + ? { payloadStore: input.taproot.payloadStore } + : {}), + ...(input.taproot.maxHydratedResourceBytes === undefined + ? {} + : { maxHydratedResourceBytes: input.taproot.maxHydratedResourceBytes }), + status: async () => status(), + }, + diamond, + authentication: (config) => { + if (config.authentication.profile !== 'local-bearer-v1') + throw new WorkshopError( + 'dependency_unavailable', + 'The local assembly supports local-bearer-v1 authentication', + 503, + ); + const credentials = createWorkshopLocalBearerCredentialStoreV1(input.db, { + async resolve(identity) { + if (identity.installationId !== input.installationId) + throw forbidden(); + const live = await operator.resolveAuthorization({ + principalId: identity.principalId, + activeWorkspaceId: identity.workspaceId, + }); + return { + capabilities: live.capabilities, + authorizationRevision: live.authorizationRevision, + }; + }, + }); + return Promise.resolve( + createAuthenticationVerifier({ + profile: 'local-bearer-v1', + credentials, + authorization: { + async recheck(principal, signal) { + signal.throwIfAborted(); + const live = await operator.resolveAuthorization({ + principalId: principal.principalId, + activeWorkspaceId: principal.activeWorkspaceId, + }); + return { ...live, credentialId: principal.credentialId }; + }, + }, + }), + ); + }, + producerBinding: { binding: producerBinding, expectation }, + trigger: createBoundedTrigger({ + intervalMs: 5_000, + timeoutMs: 4_000, + trigger: { + async run(signal) { + signal.throwIfAborted(); + const liveRegistrationContext = await operator.resolveAuthorization( + input.taproot.registrationPrincipal, + ); + await materialization.run(liveRegistrationContext, { + maxJobs: 10, + maxRebuildRoots: 10, + }); + await resumeSemanticLifecycle( + search.semantic, + liveRegistrationContext, + signal, + ); + }, + checkpoint(signal) { + signal.throwIfAborted(); + return Promise.resolve(); + }, + }, + }), + preflight: async (signal: AbortSignal) => { + signal.throwIfAborted(); + return status(); + }, + semanticConfiguration, + ...(input.assets ? { assets: input.assets } : {}), + ...(input.dependencies ? { dependencies: input.dependencies } : {}), + ownerHandles, + }; + return Object.freeze(dependencies); +} + +async function resumeSemanticLifecycle( + semantic: SemanticSearchAdminV1, + context: TaprootAuthorizationContext, + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); + const status = await semantic.status(context); + if ( + !status.configured || + status.circuitOpen || + !status.selectedConfigurationId + ) + return; + for (const plan of status.plans) { + if (plan.state !== 'approved' && plan.state !== 'running') continue; + signal.throwIfAborted(); + await semantic.resume(plan.planId, context, signal); + } +} + +function serializeMaterializationRuns( + guard: SearchMaterializationAdminGuardV1, +): SearchMaterializationAdminGuardV1 { + let tail: Promise = Promise.resolve(); + const run = ( + context: TaprootAuthorizationContext, + options: SearchMaterializationRunOptionsV1, + ) => { + const current = tail + .catch(() => undefined) + .then(() => guard.run(context, options)); + tail = current.then( + () => undefined, + () => undefined, + ); + return current; + }; + return Object.freeze({ + kind: 'taproot-search-materialization-admin-v1' as const, + initialize: (context: TaprootAuthorizationContext) => + guard.initialize(context), + run, + health: (context: TaprootAuthorizationContext) => guard.health(context), + retryDead: ( + context: TaprootAuthorizationContext, + options: { limit: number }, + ) => guard.retryDead(context, options), + startShadowRebuild: (context: TaprootAuthorizationContext) => + guard.startShadowRebuild(context), + activateReadyShadow: (context: TaprootAuthorizationContext) => + guard.activateReadyShadow(context), + }); +} + +async function createLocalOwnerHandles( + input: WorkshopLocalAssemblyPrimitivesV1, +): Promise { + const ledgerOwner = await registerMigrationLedgerOwnerV1({ + db: input.db, + installationId: input.installationId, + namespace: '@gnolith/workshop', + migrations: workshopNamespacedMigrationsV1, + assemblyAuthority: input.migrationAssemblyAuthority, + }); + return createWorkshopOwnerHandlesV1({ + db: input.db, + installationId: input.installationId, + connectionId: ledgerOwner.connectionId, + registrationFingerprint: input.registrationFingerprint, + ledgerBackup: createMigrationLedgerBackupV1( + input.db, + input.migrationAssemblyAuthority, + ), + ledgerOwner, + }); +} + +export function createWorkshopAuthorizationAuthorityV1( + db: D1DatabaseLike, + guard: InstallationAuthorizationGuard, +): WorkshopAuthorizationAuthority { + const current = async (): Promise => { + const state = await guard.readCurrentState(); + return { + installationId: state.installationId, + authorizationRevision: state.authorizationRevision, + searchGeneration: state.searchGeneration, + }; + }; + const assertCurrent = async ( + context: AuthorizationContext, + expected?: InstallationAuthorizationState, + ): Promise => { + const state = await current(); + if ( + state.installationId !== context.installationId || + state.authorizationRevision !== context.authorizationRevision || + (expected && + (state.installationId !== expected.installationId || + state.authorizationRevision !== expected.authorizationRevision || + state.searchGeneration !== expected.searchGeneration)) + ) + throw forbidden(); + }; + const batch = async ( + context: AuthorizationContext, + expected: InstallationAuthorizationState, + mutations: readonly D1PreparedStatementLike[], + ): Promise => { + await assertCurrent(context, expected); + const results = await db.batch([...mutations]); + await assertCurrent(context, expected); + return results; + }; + const authority: WorkshopAuthorizationAuthority = { + getInstallationAuthorizationState: current, + async commitTaskMutation( + _db: D1DatabaseLike, + context: AuthorizationContext, + mutation: D1PreparedStatementLike, + ) { + await assertCurrent(context); + const value = await mutation.first(); + await assertCurrent(context); + return value; + }, + async commitMemoryMutation( + _db: D1DatabaseLike, + context: AuthorizationContext, + mutation: D1PreparedStatementLike, + ) { + await assertCurrent(context); + const value = await mutation.first(); + await assertCurrent(context); + return value; + }, + commitTaskBackfill: (_db, context, expected, mutations) => + batch(context, expected, mutations), + commitMemoryBackfill: (_db, context, expected, mutations) => + batch(context, expected, mutations), + commitCursorSnapshot: (_db, context, expected, mutations) => + batch(context, expected, mutations), + }; + return Object.freeze(authority); +} + +export function createWorkshopCursorCodecV1( + secrets: WorkshopCursorSecretsV1, +): WorkshopCursorCodec { + validateCursorSecrets(secrets); + const codec: WorkshopCursorCodec = { + currentGeneration: () => Promise.resolve(secrets.generation), + async digest(purpose, value) { + const prefix = new TextEncoder().encode(`${purpose}\u0000`); + const material = new Uint8Array(prefix.length + value.length); + material.set(prefix); + material.set(value, prefix.length); + return hex( + new Uint8Array( + await crypto.subtle.sign('HMAC', secrets.digestKey, material), + ), + ); + }, + async seal(generation, plaintext) { + if (generation !== secrets.generation) throw forbidden(); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const ciphertext = new Uint8Array( + await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + secrets.encryptionKey, + plaintext as BufferSource, + ), + ); + return `wcv1.${generation}.${base64url(iv)}.${base64url(ciphertext)}`; + }, + async open(generation, token) { + const parts = token.split('.'); + if ( + parts.length !== 4 || + parts[0] !== 'wcv1' || + parts[1] !== generation || + generation !== secrets.generation + ) + throw forbidden(); + try { + return new Uint8Array( + await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: fromBase64url(parts[2]!) as BufferSource, + }, + secrets.encryptionKey, + fromBase64url(parts[3]!) as BufferSource, + ), + ); + } catch { + throw forbidden(); + } + }, + }; + return Object.freeze(codec); +} + +export function createWorkshopDiamondSparqlPortV1( + db: D1DatabaseLike, + options: Omit = {}, +): DiamondSparqlPort { + const port: DiamondSparqlPort = { + async validate(query) { + const { ContextQueryValidator } = await import('./server/sparql.js'); + return new ContextQueryValidator().validate(query); + }, + async query(scopedView, query, request) { + const policy = scopedPolicy(scopedView); + const allowed = new Set(policy.filter.allowedSubjectIris); + if (request.dryRun) + return { + valid: true, + dryRun: true, + datasetId: policy.datasetId, + allowedSubjectCount: allowed.size, + }; + const execute = createSparqlExecutor({ + ...options, + db, + sourceFactory: (sourceDb, sourceOptions) => + scopedSource(new D1QuadSource(sourceDb, sourceOptions), allowed), + }); + const response = await execute({ + operation: 'query', + text: query, + accept: 'application/sparql-results+json, application/n-quads', + signal: request.signal, + }); + const bytes = response.body + ? new Uint8Array(await new Response(response.body).arrayBuffer()) + : new Uint8Array(); + const text = new TextDecoder().decode(bytes); + return { + status: response.status, + mediaType: response.mediaType ?? 'application/octet-stream', + data: parseResult(text, response.mediaType), + byteLength: bytes.byteLength, + ...(request.limit === undefined + ? {} + : { requestedLimit: request.limit }), + }; + }, + }; + return Object.freeze(port); +} + +function scopedSource( + source: D1QuadSource, + allowed: ReadonlySet, +): RDF.Source { + return { + match( + subject?: RDF.Term | null, + predicate?: RDF.Term | null, + object?: RDF.Term | null, + graph?: RDF.Term | null, + ) { + if (subject?.termType === 'NamedNode' && !allowed.has(subject.value)) + return Readable.from([], { + objectMode: true, + }) as RDF.Stream; + const filter = new Transform({ + objectMode: true, + transform( + quad: RDF.Quad, + _encoding: BufferEncoding, + callback: TransformCallback, + ) { + callback(null, allowed.has(quad.subject.value) ? quad : undefined); + }, + }); + const matches = source.match( + subject, + predicate, + object, + graph, + ) as unknown as Readable; + return matches.pipe(filter) as RDF.Stream; + }, + }; +} + +function scopedPolicy(value: unknown): { + kind: 'taproot-scoped-sparql-policy-v1'; + datasetId: string; + filter: { + format: 'taproot-sparql-graph-filter-v1'; + allowedSubjectIris: readonly string[]; + }; +} { + if ( + !value || + typeof value !== 'object' || + (value as { kind?: unknown }).kind !== 'taproot-scoped-sparql-policy-v1' + ) + throw forbidden(); + return value as ReturnType; +} + +async function workshopLocalStatus( + db: D1DatabaseLike, + guard: InstallationAuthorizationGuard, + input: WorkshopLocalAssemblyPrimitivesV1, + expectation: WorkshopProducerBindingExpectation, + blobReady: boolean, + materializationHealth: () => Promise<{ + status: string; + sourceHighWatermark: number; + activeAppliedWatermark: number; + }>, + semanticHealth: () => Promise<{ + configured: boolean; + state: string; + revision: number | null; + fingerprint: string | null; + ready: boolean; + circuitOpen: boolean; + failureCode: string | null; + }>, +): Promise { + const [schema, state, searchHealth, persistedSemantic, semanticHealthValue] = + await Promise.all([ + db + .prepare('SELECT version FROM workshop_schema WHERE singleton = 1') + .first<{ version: number }>(), + guard.readCurrentState(), + materializationHealth(), + readWorkshopSemanticStatusV1(db, input.installationId), + semanticHealth(), + ]); + const authorizationReady = + state.installationId === input.installationId && + state.authorizationRevision >= 1; + const semanticReady = semanticHealthValue.ready; + return { + baseIri: input.taproot.options.baseIri, + migrations: { + namespace: '@gnolith/workshop', + version: schema?.version ?? 0, + ready: schema?.version === 11, + }, + compatibility: { diamond: true, taproot: true }, + canonicalReady: true, + authorizationReady, + lexicalReady: + searchHealth.status === 'healthy' && + searchHealth.activeAppliedWatermark >= searchHealth.sourceHighWatermark, + semantic: { + state: semanticReady + ? 'ready' + : semanticHealthValue.configured || persistedSemantic.configured + ? 'degraded' + : 'unconfigured', + configured: + semanticHealthValue.configured || persistedSemantic.configured, + revision: semanticHealthValue.revision ?? persistedSemantic.revision, + fingerprint: + semanticHealthValue.fingerprint ?? persistedSemantic.fingerprint, + ready: semanticReady, + diagnostic: + !semanticHealthValue.configured && !persistedSemantic.configured + ? null + : semanticReady + ? null + : { + format: 'gnolith-workshop-semantic-diagnostic-v1', + version: 1, + code: + semanticHealthValue.circuitOpen || + semanticHealthValue.failureCode !== null + ? 'provider-unavailable' + : 'materialization-pending', + retryable: true, + backend: persistedSemantic.backend!, + repair: 'retry', + }, + }, + producers: { + ready: true, + fingerprint: expectation.registrationFingerprint, + kinds: ['task', 'memory', 'prompt'], + }, + blobReady, + }; +} + +function producerHandle( + input: WorkshopLocalAssemblyPrimitivesV1, + expectation: WorkshopProducerBindingExpectation, + handle: unknown, +) { + return { + owner: '@gnolith/workshop' as const, + installationId: input.installationId, + connectionId: expectation.connectionId, + registrationFingerprint: input.registrationFingerprint, + handle, + }; +} + +function toTaprootContext( + context: AuthorizationContext, +): TaprootAuthorizationContext { + return { + installationId: context.installationId, + principalId: context.principalId, + activeWorkspaceId: context.activeWorkspaceId, + workspaceIds: context.workspaceIds, + capabilities: context.capabilities, + authorizationRevision: context.authorizationRevision, + }; +} + +function validateInput(input: WorkshopLocalAssemblyPrimitivesV1): void { + if ( + input.kind !== 'workshop-local-assembly-primitives-v1' || + !input.version || + !input.installationId || + !/^[a-f0-9]{64}$/u.test(input.registrationFingerprint) || + !input.taproot.registrationPrincipal.principalId + ) + throw new WorkshopError( + 'dependency_unavailable', + 'Workshop local assembly primitives are incomplete', + 503, + ); +} + +function validateCursorSecrets(secrets: WorkshopCursorSecretsV1): void { + if ( + !secrets.generation || + secrets.encryptionKey.extractable || + secrets.encryptionKey.algorithm.name !== 'AES-GCM' || + !secrets.encryptionKey.usages.includes('encrypt') || + !secrets.encryptionKey.usages.includes('decrypt') || + secrets.digestKey.extractable || + secrets.digestKey.algorithm.name !== 'HMAC' || + !secrets.digestKey.usages.includes('sign') + ) + throw new WorkshopError( + 'dependency_unavailable', + 'Workshop cursor keys are incompatible', + 503, + ); +} + +function parseResult(text: string, mediaType?: string): unknown { + if (mediaType?.includes('json')) { + try { + return JSON.parse(text); + } catch { + throw new WorkshopError( + 'internal_error', + 'Diamond returned invalid JSON', + 500, + ); + } + } + return text; +} + +function base64url(value: Uint8Array): string { + return Buffer.from(value).toString('base64url'); +} + +function fromBase64url(value: string): Uint8Array { + if (!/^[A-Za-z0-9_-]+$/u.test(value)) throw forbidden(); + return new Uint8Array(Buffer.from(value, 'base64url')); +} + +function hex(value: Uint8Array): string { + return [...value].map((byte) => byte.toString(16).padStart(2, '0')).join(''); +} + +function forbidden(): WorkshopError { + return new WorkshopError('forbidden', 'Authorization denied', 403); +} diff --git a/src/mcp.ts b/src/mcp.ts index e75e413..404df64 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -1,3 +1,12 @@ -export * from './mcp/tools.js'; -export * from './mcp/dispatcher.js'; -export * from './mcp/server.js'; +export { + GNOLITH_MCP_PROTOCOL_VERSION, + createGnolithMcpServer, + type GnolithMcpServer, + type GnolithMcpServerOptions, +} from './mcp/server.js'; +export { + CATALOG_DIGEST, + OPERATION_SCHEMA_VERSION, + operationCatalog, + type OperationName, +} from './generated/operations.js'; diff --git a/src/mcp/dispatcher.ts b/src/mcp/dispatcher.ts deleted file mode 100644 index 9691960..0000000 --- a/src/mcp/dispatcher.ts +++ /dev/null @@ -1,527 +0,0 @@ -import type { KnowledgeService } from '../protocol/knowledge.js'; -import { - WorkshopError, - normalizeWorkshopError, - type WorkshopErrorBody, -} from '../protocol/errors.js'; -import { hasWorkshopCapability, type WorkshopPrincipal } from '../protocol.js'; -import { authorize } from '../server/authorization.js'; -import { KNOWLEDGE_READ_TOOL_NAMES } from '../server/knowledge.js'; -import type { WorkshopLimits } from '../server/limits.js'; -import { resolveLimits } from '../server/limits.js'; -import type { MemoryService } from '../server/memories.js'; -import { - observed, - observeSafely, - type ObserveWorkshop, -} from '../server/observability.js'; -import type { PacketService } from '../server/packets.js'; -import type { TaskService } from '../server/tasks.js'; -import type { PromptService } from '../server/prompts.js'; -import type { WorkshopSearchIntegrationV1 } from '../server/search.js'; -import { workshopTools, type WorkshopToolDefinition } from './tools.js'; - -/** The transport-neutral services needed to execute Workshop tools. */ -export interface WorkshopToolRuntime { - readonly tasks: TaskService; - readonly memories: MemoryService; - readonly prompts?: PromptService; - readonly search?: WorkshopSearchIntegrationV1; - readonly packets: PacketService; - readonly knowledge: KnowledgeService; - readonly limits?: Partial; - readonly observe?: ObserveWorkshop; -} - -export interface WorkshopToolCall { - readonly name: string; - readonly arguments?: Readonly>; -} - -export interface WorkshopToolDispatchContext { - readonly principal: WorkshopPrincipal | null; - readonly requestId: string; - readonly signal?: AbortSignal; -} - -export type WorkshopDispatchErrorKind = - | 'unauthenticated' - | 'forbidden' - | 'unknown_tool' - | 'invalid_arguments' - | 'operation'; - -export interface WorkshopDispatchFailure { - readonly kind: WorkshopDispatchErrorKind; - readonly error: WorkshopErrorBody; -} - -export type WorkshopDispatchResult = - | { readonly ok: true; readonly value: T } - | { readonly ok: false; readonly failure: WorkshopDispatchFailure }; - -export interface WorkshopToolDispatcher { - readonly tools: readonly WorkshopToolDefinition[]; - listTools( - principal: WorkshopPrincipal | null, - ): WorkshopDispatchResult; - callTool( - call: WorkshopToolCall, - context: WorkshopToolDispatchContext, - ): Promise>; -} - -const DIRECT_TOOL_NAMES = [ - 'list_tasks', - 'search_tasks', - 'get_task_packet', - 'create_task', - 'update_task', - 'archive_task', - 'claim_task', - 'complete_task', - 'task_history', - 'list_memories', - 'get_memory', - 'upsert_memory', - 'memory_history', - 'list_prompts', - 'get_prompt', - 'create_prompt', - 'update_prompt', - 'delete_prompt', - 'prompt_history', - 'search', - 'search_status', - 'search_admin', -] as const; - -const KNOWLEDGE_TOOL_NAME_SET: ReadonlySet = new Set( - KNOWLEDGE_READ_TOOL_NAMES, -); -const HANDLED_TOOL_NAMES: ReadonlySet = new Set([ - ...DIRECT_TOOL_NAMES, - ...KNOWLEDGE_READ_TOOL_NAMES, -]); - -export function createWorkshopToolDispatcher( - runtime: WorkshopToolRuntime, -): WorkshopToolDispatcher { - assertCompleteRegistry(); - const definitions = new Map( - workshopTools.map((definition) => [definition.name, definition] as const), - ); - const availableTools = workshopTools; - - return { - tools: availableTools, - listTools(principal) { - const authorized = authorizeResult(principal, 'read'); - if (!authorized.ok) return authorized; - return { - ok: true, - value: availableTools.filter((tool) => - hasCapability(authorized.value, tool.capability), - ), - }; - }, - async callTool(call, context) { - if (!context.principal) { - return authorizationFailure( - new WorkshopError('unauthenticated', 'Authentication is required'), - ); - } - const definition = definitions.get(call.name); - if (!definition) { - return { - ok: false, - failure: { - kind: 'unknown_tool', - error: new WorkshopError( - 'bad_request', - `Unknown tool ${call.name}`, - 400, - { tool: call.name }, - ).toJSON(), - }, - }; - } - const authorized = authorizeResult( - context.principal, - definition.capability, - ); - if (!authorized.ok) { - await observeAuthorizationFailure(runtime, authorized.failure, context); - return authorized; - } - - try { - const value = await observed( - runtime.observe, - { - operation: 'mcp.call', - principalId: authorized.value.principalId, - requestId: context.requestId, - }, - () => - withTimeout( - executeTool( - runtime, - authorized.value, - call.name, - { ...(call.arguments ?? {}) }, - context.requestId, - context.signal ?? new AbortController().signal, - ), - resolveLimits(runtime.limits).toolTimeoutMs, - ), - ); - return { ok: true, value }; - } catch (error) { - const normalized = normalizeWorkshopError(error); - if ( - normalized.code === 'unauthenticated' || - normalized.code === 'forbidden' - ) { - const failure = authorizationFailure(normalized); - if (!failure.ok) - await observeAuthorizationFailure( - runtime, - failure.failure, - context, - ); - return failure; - } - return { - ok: false, - failure: { - kind: - normalized.code === 'bad_request' || - normalized.code === 'validation_failed' - ? 'invalid_arguments' - : 'operation', - error: normalized.toJSON(), - }, - }; - } - }, - }; -} - -async function executeTool( - runtime: WorkshopToolRuntime, - principal: WorkshopPrincipal, - name: string, - input: Record, - requestId: string, - signal: AbortSignal, -): Promise { - switch (name) { - case 'list_tasks': - case 'search_tasks': - return runtime.tasks.list(input, principal); - case 'get_task_packet': - return runtime.packets.get(text(input.id, 'id'), principal, signal); - case 'create_task': - return runtime.tasks.create(input as never, principal); - case 'update_task': { - const { id, ...update } = input; - return runtime.tasks.update(text(id, 'id'), update as never, principal); - } - case 'archive_task': { - const { id, expectedRevision, expectedUpdatedAt } = input; - return runtime.tasks.archive( - text(id, 'id'), - { - ...(expectedRevision === undefined ? {} : { expectedRevision }), - ...(expectedUpdatedAt === undefined ? {} : { expectedUpdatedAt }), - } as never, - principal, - ); - } - case 'claim_task': - return runtime.tasks.claim(text(input.id, 'id'), principal); - case 'complete_task': - return runtime.tasks.complete( - text(input.id, 'id'), - input.result, - principal, - ); - case 'task_history': - return runtime.tasks.history(text(input.id, 'id'), principal, { - ...(input.limit === undefined - ? {} - : { limit: positive(input.limit, 'limit') }), - }); - case 'list_memories': - return runtime.memories.list(input, principal); - case 'get_memory': - return runtime.memories.get(text(input.slug, 'slug'), principal); - case 'upsert_memory': { - const { slug, ...memory } = input; - return runtime.memories.upsert( - text(slug, 'slug'), - memory as never, - principal, - ); - } - case 'memory_history': - return runtime.memories.history(text(input.slug, 'slug'), principal, { - ...(input.limit === undefined - ? {} - : { limit: positive(input.limit, 'limit') }), - }); - case 'list_prompts': - return requiredPrompts(runtime).list(input, principal); - case 'get_prompt': - return requiredPrompts(runtime).get(text(input.id, 'id'), principal); - case 'create_prompt': - return requiredPrompts(runtime).create(input as never, principal); - case 'update_prompt': { - const { id, ...update } = input; - return requiredPrompts(runtime).update( - text(id, 'id'), - update as never, - principal, - ); - } - case 'delete_prompt': - return requiredPrompts(runtime).delete( - text(input.id, 'id'), - positive(input.expectedRevision, 'expectedRevision'), - principal, - ); - case 'prompt_history': - return requiredPrompts(runtime).history(text(input.id, 'id'), principal, { - ...(input.limit === undefined - ? {} - : { limit: positive(input.limit, 'limit') }), - }); - case 'search': - return requiredSearch(runtime).search(input as never, principal); - case 'search_status': { - const search = requiredSearch(runtime); - return { - materialization: await search.materialization.health(principal), - semantic: await search.semantic.status(principal), - }; - } - case 'search_admin': - return executeSearchAdmin( - requiredSearch(runtime), - input, - principal, - signal, - ); - } - - if (KNOWLEDGE_TOOL_NAME_SET.has(name)) { - return runtime.knowledge.call({ name, input }, { ...principal, requestId }); - } - throw new WorkshopError('internal_error', `Tool ${name} has no handler`); -} - -function requiredPrompts(runtime: WorkshopToolRuntime): PromptService { - if (!runtime.prompts) - throw new WorkshopError( - 'internal_error', - 'Prompt producer is not registered', - ); - return runtime.prompts; -} - -function requiredSearch( - runtime: WorkshopToolRuntime, -): WorkshopSearchIntegrationV1 { - if (!runtime.search) - throw new WorkshopError( - 'internal_error', - 'Search integration is not registered', - ); - return runtime.search; -} - -async function executeSearchAdmin( - search: WorkshopSearchIntegrationV1, - input: Record, - principal: WorkshopPrincipal, - signal: AbortSignal, -): Promise { - const operation = text(input.operation, 'operation'); - const id = () => text(input.configurationId, 'configurationId'); - const plan = () => text(input.planId, 'planId'); - switch (operation) { - case 'materialize': - return search.materialization.run(principal, { - maxJobs: positive(input.maxJobs ?? 20, 'maxJobs'), - maxRebuildRoots: positive( - input.maxRebuildRoots ?? 100, - 'maxRebuildRoots', - ), - }); - case 'retry-dead': - return search.materialization.retryDead(principal, { - limit: positive(input.limit ?? 20, 'limit'), - }); - case 'adopt-legacy': { - const kind = text(input.kind, 'kind'); - if (kind !== 'task' && kind !== 'memory' && kind !== 'prompt') - throw new WorkshopError('validation_failed', 'kind is invalid'); - return search[kind].producer.adoptLegacyPage(principal, { - limit: positive(input.limit ?? 100, 'limit'), - }); - } - case 'start-rebuild': - return search.materialization.startShadowRebuild(principal); - case 'activate-rebuild': - return search.materialization.activateReadyShadow(principal); - case 'semantic-select': - return search.semantic.select(id(), principal); - case 'semantic-reconnect': - return search.semantic.reconnect(id(), principal); - case 'semantic-estimate': - return search.semantic.estimate(id(), input.policy as never, principal); - case 'semantic-approve': - return search.semantic.approve(plan(), principal); - case 'semantic-run': - return search.semantic.run(plan(), principal, signal); - case 'semantic-resume': - return search.semantic.resume(plan(), principal, signal); - case 'semantic-pause': - return search.semantic.pause(plan(), principal); - case 'semantic-stop': - return search.semantic.stop(plan(), principal); - case 'semantic-retry': - return search.semantic.retry(plan(), principal); - case 'semantic-exclude': - return search.semantic.exclude( - id(), - positive(input.generation, 'generation'), - text(input.derivedId, 'derivedId'), - text(input.reason, 'reason'), - principal, - ); - case 'semantic-retire': - return search.semantic.retire(id(), principal); - case 'semantic-delete-embeddings': - return search.semantic.deleteEmbeddings(id(), principal); - default: - throw new WorkshopError( - 'validation_failed', - 'search admin operation is invalid', - ); - } -} - -function authorizeResult( - principal: WorkshopPrincipal | null, - capability: WorkshopToolDefinition['capability'], -): WorkshopDispatchResult { - try { - return { ok: true, value: authorize(principal, capability) }; - } catch (error) { - return authorizationFailure(normalizeWorkshopError(error)); - } -} - -function authorizationFailure( - error: WorkshopError, -): WorkshopDispatchResult { - return { - ok: false, - failure: { - kind: error.code === 'unauthenticated' ? 'unauthenticated' : 'forbidden', - error: error.toJSON(), - }, - }; -} - -async function observeAuthorizationFailure( - runtime: WorkshopToolRuntime, - failure: WorkshopDispatchFailure, - context: WorkshopToolDispatchContext, -): Promise { - await observeSafely(runtime.observe, { - operation: 'authorization.failure', - outcome: 'error', - durationMs: 0, - errorCode: failure.error.code, - requestId: context.requestId, - ...(context.principal - ? { principalId: context.principal.principalId } - : {}), - }); -} - -function hasCapability( - principal: WorkshopPrincipal, - capability: WorkshopToolDefinition['capability'], -): boolean { - return hasWorkshopCapability(principal.capabilities, capability); -} - -function assertCompleteRegistry(): void { - const schemaNames = new Set(); - for (const definition of workshopTools) { - if (schemaNames.has(definition.name)) { - throw new WorkshopError( - 'internal_error', - `Duplicate Workshop tool ${definition.name}`, - ); - } - schemaNames.add(definition.name); - if (!HANDLED_TOOL_NAMES.has(definition.name)) { - throw new WorkshopError( - 'internal_error', - `Workshop tool ${definition.name} has no handler`, - ); - } - } - for (const name of HANDLED_TOOL_NAMES) { - if (!schemaNames.has(name)) { - throw new WorkshopError( - 'internal_error', - `Workshop handler ${name} has no tool definition`, - ); - } - } -} - -function text(value: unknown, field: string): string { - if (typeof value !== 'string' || !value.trim()) { - throw new WorkshopError('validation_failed', `${field} is required`, 400, { - field, - }); - } - return value.trim(); -} - -function positive(value: unknown, field: string): number { - if (!Number.isSafeInteger(value) || Number(value) < 1) - throw new WorkshopError( - 'validation_failed', - `${field} must be a positive integer`, - ); - return Number(value); -} - -async function withTimeout( - promise: Promise, - milliseconds: number, -): Promise { - let timer: ReturnType | undefined; - try { - return await Promise.race([ - promise, - new Promise((_resolve, reject) => { - timer = setTimeout( - () => - reject(new WorkshopError('query_timeout', 'Tool call timed out')), - milliseconds, - ); - }), - ]); - } finally { - if (timer) clearTimeout(timer); - } -} diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 87e1d87..41cb95a 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -1,21 +1,18 @@ -import { WorkshopError, normalizeWorkshopError } from '../protocol/errors.js'; -import type { WorkshopPrincipal } from '../protocol.js'; -import type { WorkshopRuntime } from '../server/context.js'; -import { resolveLimits } from '../server/limits.js'; -import { observeSafely } from '../server/observability.js'; import { - createWorkshopToolDispatcher, - type WorkshopDispatchFailure, - type WorkshopToolDispatcher, -} from './dispatcher.js'; -import type { WorkshopToolDefinition } from './tools.js'; + operationCatalog, + type OperationName, +} from '../generated/operations.js'; +import type { OperationRuntime } from '../operations.js'; +import { WorkshopError, normalizeWorkshopError } from '../protocol/errors.js'; +import type { AuthenticatedPrincipal } from '../protocol.js'; -export const WORKSHOP_MCP_PROTOCOL_VERSION = '2025-11-25'; +export const GNOLITH_MCP_PROTOCOL_VERSION = '2025-11-25'; const SUPPORTED_PROTOCOLS = new Set([ - WORKSHOP_MCP_PROTOCOL_VERSION, + GNOLITH_MCP_PROTOCOL_VERSION, '2025-06-18', '2025-03-26', ]); +const DEFAULT_MAX_REQUEST_BYTES = 1024 * 1024; type RequestId = string | number; @@ -24,148 +21,133 @@ interface JsonRpcMessage { id?: RequestId | null; method?: string; params?: unknown; - result?: unknown; - error?: unknown; } -export interface WorkshopMcpServer { - readonly tools: readonly WorkshopToolDefinition[]; +export interface GnolithMcpServerOptions { + version: string; + operations: OperationRuntime; + authenticate(request: Request): Promise; + allowedOrigins?: readonly string[]; + maxRequestBytes?: number; + challenge?: string; +} + +export interface GnolithMcpServer { + readonly tools: typeof operationCatalog; handle(request: Request): Promise; } -export function createWorkshopMcpServer( - runtime: WorkshopRuntime, -): WorkshopMcpServer { - const limits = resolveLimits(runtime.limits); - const dispatcher = createWorkshopToolDispatcher(runtime); +export function createGnolithMcpServer( + options: GnolithMcpServerOptions, +): GnolithMcpServer { + const origins = new Set(options.allowedOrigins ?? []); + const maxBytes = options.maxRequestBytes ?? DEFAULT_MAX_REQUEST_BYTES; return { - tools: dispatcher.tools, + tools: operationCatalog, async handle(request) { - if (request.method === 'GET') { - return new Response('SSE listening is not enabled', { - status: 405, - headers: { allow: 'POST, OPTIONS' }, - }); - } - if (request.method === 'OPTIONS') return corsPreflight(request, runtime); - if (request.method !== 'POST') { - return new Response('Method not allowed', { - status: 405, - headers: { allow: 'GET, POST, OPTIONS' }, - }); - } - const originFailure = validateOrigin(request, runtime); + if (request.method === 'GET') return methodNotAllowed('POST, OPTIONS'); + if (request.method === 'OPTIONS') return preflight(request, origins); + if (request.method !== 'POST') return methodNotAllowed('POST, OPTIONS'); + const originFailure = validateOrigin(request, origins); if (originFailure) return originFailure; const contentType = request.headers.get('content-type')?.split(';', 1)[0]; - if (contentType !== 'application/json') { + if (contentType !== 'application/json') return httpError(415, 'MCP requests require application/json'); - } const accept = request.headers.get('accept') ?? ''; if ( !accept.includes('application/json') || !accept.includes('text/event-stream') - ) { + ) return httpError( 406, 'MCP requests must accept application/json and text/event-stream', ); + + let principal: AuthenticatedPrincipal; + try { + principal = await options.authenticate(request); + } catch { + return unauthenticated(options.challenge); } + let message: JsonRpcMessage; try { - message = await readMessage(request, limits.maxMcpRequestBytes); + message = await readMessage(request, maxBytes); } catch (error) { const normalized = normalizeWorkshopError(error); - await observeSafely(runtime.observe, { - operation: 'authorization.failure', - outcome: 'error', - durationMs: 0, - errorCode: normalized.code, - ...(request.headers.get('x-request-id') - ? { requestId: request.headers.get('x-request-id')! } - : {}), - }); return rpcResponse( - rpcError(null, -32700, normalized.message, normalized.toJSON()), + rpcError(null, -32700, normalized.message), normalized.status, request, - runtime, + origins, ); } if ( message.jsonrpc !== '2.0' || typeof message.method !== 'string' || Array.isArray(message) - ) { + ) return rpcResponse( rpcError(message.id ?? null, -32600, 'Invalid JSON-RPC request'), 400, request, - runtime, + origins, ); - } + const protocol = + request.headers.get('mcp-protocol-version') ?? '2025-03-26'; if ( message.method !== 'initialize' && - message.method !== 'notifications/initialized' - ) { - const protocol = - request.headers.get('mcp-protocol-version') ?? '2025-03-26'; - if (!SUPPORTED_PROTOCOLS.has(protocol)) { - return httpError(400, 'Unsupported MCP protocol version'); - } - } - let principal: WorkshopPrincipal | null; - try { - principal = await runtime.resolvePrincipal(request); - if (!principal) { - throw new WorkshopError( - 'unauthenticated', - 'MCP authentication is required', - ); - } - } catch (error) { - const normalized = normalizeWorkshopError(error); - return rpcResponse( - rpcError(message.id ?? null, -32001, normalized.message), - normalized.status, - request, - runtime, - ); - } + message.method !== 'notifications/initialized' && + !SUPPORTED_PROTOCOLS.has(protocol) + ) + return httpError(400, 'Unsupported MCP protocol version'); + if (message.id === undefined || message.id === null) { return new Response(null, { status: 202, - headers: corsHeaders(request, runtime), + headers: responseHeaders(request, origins), }); } + try { - const result = await dispatch( - runtime, - dispatcher, - principal, - message, - request, - ); + const result = await dispatch(message, request, principal, options); return rpcResponse( { jsonrpc: '2.0', id: message.id, result }, 200, request, - runtime, + origins, ); } catch (error) { - if (error instanceof ProtocolError) { + if (error instanceof ProtocolError) return rpcResponse( rpcError(message.id, error.rpcCode, error.message), 200, request, - runtime, + origins, ); - } const normalized = normalizeWorkshopError(error); + const operationFailure = + normalized.code === 'unauthenticated' || + normalized.code === 'forbidden' + ? { code: -32001, message: normalized.message } + : { + code: -32603, + message: + normalized.code === 'internal_error' + ? 'Workshop operation failed' + : normalized.message, + data: normalized.toJSON(), + }; return rpcResponse( - rpcError(message.id, -32603, normalized.message), + rpcError( + message.id, + operationFailure.code, + operationFailure.message, + 'data' in operationFailure ? operationFailure.data : undefined, + ), 200, request, - runtime, + origins, ); } }, @@ -173,11 +155,10 @@ export function createWorkshopMcpServer( } async function dispatch( - runtime: WorkshopRuntime, - dispatcher: WorkshopToolDispatcher, - principal: WorkshopPrincipal, message: JsonRpcMessage, request: Request, + principal: AuthenticatedPrincipal, + options: GnolithMcpServerOptions, ): Promise { switch (message.method) { case 'initialize': { @@ -185,69 +166,60 @@ async function dispatch( const requested = typeof params.protocolVersion === 'string' ? params.protocolVersion - : WORKSHOP_MCP_PROTOCOL_VERSION; + : GNOLITH_MCP_PROTOCOL_VERSION; return { protocolVersion: SUPPORTED_PROTOCOLS.has(requested) ? requested - : WORKSHOP_MCP_PROTOCOL_VERSION, + : GNOLITH_MCP_PROTOCOL_VERSION, capabilities: { tools: { listChanged: false } }, - serverInfo: { name: '@gnolith/workshop', version: '0.4.2' }, + serverInfo: { name: 'gnolith', version: options.version }, instructions: - 'Search existing tasks before creating overlapping work. Reading a task packet never claims it. All knowledge writes require the latest Taproot revision.', + 'Gnolith exposes one authenticated operation catalog. Search before creating; use exact revisions and task lease tokens for mutations.', }; } case 'ping': return {}; - case 'tools/list': { - const listed = dispatcher.listTools(principal); - if (!listed.ok) throw dispatchFailure(listed.failure); + case 'tools/list': return { - tools: listed.value.map((tool) => ({ - name: tool.name, - title: tool.title, - description: tool.description, - inputSchema: tool.inputSchema, + tools: operationCatalog.map((definition) => ({ + name: definition.name, + title: title(definition.name), + description: `${definition.name} v${definition.version}`, + inputSchema: definition.inputSchema, + annotations: { + readOnlyHint: definition.annotations.readOnly, + destructiveHint: definition.annotations.destructive, + idempotentHint: definition.annotations.idempotent, + }, })), }; - } case 'tools/call': { const params = asObject(message.params); const name = typeof params.name === 'string' ? params.name : ''; + if (!operationCatalog.some((operation) => operation.name === name)) + throw new ProtocolError(-32602, 'Unknown operation'); const input = params.arguments === undefined ? {} : asObject(params.arguments); const requestId = - request.headers.get('x-request-id') ?? crypto.randomUUID(); - const called = await dispatcher.callTool( - { name, arguments: input }, - { principal, requestId, signal: request.signal }, + boundedRequestId(request.headers.get('x-request-id')) ?? + crypto.randomUUID(); + const value = await options.operations.call( + name as OperationName, + input, + { + principal, + requestId, + signal: request.signal, + }, ); - if (!called.ok) { - if (called.failure.kind === 'unknown_tool') { - throw new ProtocolError(-32602, called.failure.error.message); - } - if ( - called.failure.kind === 'unauthenticated' || - called.failure.kind === 'forbidden' - ) { - throw dispatchFailure(called.failure); - } - return { - content: [{ type: 'text', text: called.failure.error.message }], - structuredContent: { error: called.failure.error }, - isError: true, - }; - } return { - content: [{ type: 'text', text: JSON.stringify(called.value) }], - structuredContent: toStructured(called.value), + content: [{ type: 'text', text: JSON.stringify(value) }], + structuredContent: toStructured(value), isError: false, }; } default: - throw new ProtocolError( - -32601, - `Method ${message.method} is not supported`, - ); + throw new ProtocolError(-32601, 'Method not supported'); } } @@ -256,9 +228,8 @@ async function readMessage( maxBytes: number, ): Promise { const declared = Number(request.headers.get('content-length')); - if (Number.isFinite(declared) && declared > maxBytes) { - throw new WorkshopError('limit_exceeded', 'MCP request is too large'); - } + if (Number.isFinite(declared) && declared > maxBytes) + throw new WorkshopError('limit_exceeded', 'MCP request is too large', 413); if (!request.body) throw new WorkshopError('bad_request', 'Missing MCP request body'); const reader = request.body.getReader(); @@ -270,7 +241,11 @@ async function readMessage( total += next.value.byteLength; if (total > maxBytes) { await reader.cancel(); - throw new WorkshopError('limit_exceeded', 'MCP request is too large'); + throw new WorkshopError( + 'limit_exceeded', + 'MCP request is too large', + 413, + ); } chunks.push(next.value); } @@ -281,35 +256,46 @@ async function readMessage( offset += chunk.byteLength; } try { - return JSON.parse(new TextDecoder().decode(bytes)) as JsonRpcMessage; - } catch { + const parsed = JSON.parse(new TextDecoder().decode(bytes)) as unknown; + if (Array.isArray(parsed)) + throw new WorkshopError( + 'bad_request', + 'Batched JSON-RPC is not supported', + ); + return parsed as JsonRpcMessage; + } catch (error) { + if (error instanceof WorkshopError) throw error; throw new WorkshopError('bad_request', 'Invalid JSON request body'); } } function validateOrigin( request: Request, - runtime: WorkshopRuntime, + origins: ReadonlySet, ): Response | null { const origin = request.headers.get('origin'); if (!origin) return null; - const allowed = resolveCorsOrigin(request, runtime); - if (origin !== new URL(request.url).origin && origin !== allowed) { + if (origin !== new URL(request.url).origin && !origins.has(origin)) return httpError(403, 'Origin is not allowed'); - } return null; } -function resolveCorsOrigin( - request: Request, - runtime: WorkshopRuntime, -): string | null { - return typeof runtime.corsOrigin === 'function' - ? runtime.corsOrigin(request) - : (runtime.corsOrigin ?? null); +function preflight(request: Request, origins: ReadonlySet): Response { + const failure = validateOrigin(request, origins); + if (failure) return failure; + const headers = responseHeaders(request, origins); + headers.set('access-control-allow-methods', 'POST, OPTIONS'); + headers.set( + 'access-control-allow-headers', + 'Authorization, Content-Type, Accept, MCP-Protocol-Version, X-Request-Id', + ); + return new Response(null, { status: 204, headers }); } -function corsHeaders(request: Request, runtime: WorkshopRuntime): Headers { +function responseHeaders( + request: Request, + origins: ReadonlySet, +): Headers { const headers = new Headers({ 'cache-control': 'no-store', 'x-content-type-options': 'nosniff', @@ -317,8 +303,7 @@ function corsHeaders(request: Request, runtime: WorkshopRuntime): Headers { const origin = request.headers.get('origin'); if ( origin && - (origin === new URL(request.url).origin || - origin === resolveCorsOrigin(request, runtime)) + (origin === new URL(request.url).origin || origins.has(origin)) ) { headers.set('access-control-allow-origin', origin); headers.set('vary', 'Origin'); @@ -326,31 +311,42 @@ function corsHeaders(request: Request, runtime: WorkshopRuntime): Headers { return headers; } -function corsPreflight(request: Request, runtime: WorkshopRuntime): Response { - const failure = validateOrigin(request, runtime); - if (failure) return failure; - const headers = corsHeaders(request, runtime); - headers.set('access-control-allow-methods', 'POST, OPTIONS'); - headers.set( - 'access-control-allow-headers', - 'Authorization, Content-Type, Accept, MCP-Protocol-Version, X-Request-Id', - ); - return new Response(null, { status: 204, headers }); -} - function rpcResponse( body: unknown, status: number, request: Request, - runtime: WorkshopRuntime, + origins: ReadonlySet, ): Response { - const headers = corsHeaders(request, runtime); + const headers = responseHeaders(request, origins); headers.set('content-type', 'application/json; charset=utf-8'); return new Response(JSON.stringify(body), { status, headers }); } +function unauthenticated(challenge = 'Bearer'): Response { + return Response.json( + { error: { code: 'unauthenticated', message: 'Authentication required' } }, + { + status: 401, + headers: { + 'cache-control': 'no-store', + 'www-authenticate': challenge, + }, + }, + ); +} + function httpError(status: number, message: string): Response { - return Response.json({ error: message }, { status }); + return Response.json( + { error: { code: 'bad_request', message } }, + { status, headers: { 'cache-control': 'no-store' } }, + ); +} + +function methodNotAllowed(allow: string): Response { + return new Response('Method not allowed', { + status: 405, + headers: { allow }, + }); } function rpcError( @@ -358,7 +354,7 @@ function rpcError( code: number, message: string, data?: unknown, -): JsonRpcMessage { +): Record { return { jsonrpc: '2.0', id, @@ -367,9 +363,8 @@ function rpcError( } function asObject(value: unknown): Record { - if (!value || typeof value !== 'object' || Array.isArray(value)) { + if (!value || typeof value !== 'object' || Array.isArray(value)) throw new ProtocolError(-32602, 'Expected an object'); - } return value as Record; } @@ -379,13 +374,17 @@ function toStructured(value: unknown): Record { : { value }; } -function dispatchFailure(failure: WorkshopDispatchFailure): WorkshopError { - return new WorkshopError( - failure.error.code, - failure.error.message, - undefined, - failure.error.details, - ); +function boundedRequestId(value: string | null): string | null { + if (!value || value.length > 256 || /[\u0000-\u001f\u007f]/u.test(value)) + return null; + return value; +} + +function title(name: string): string { + return name + .split('_') + .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)) + .join(' '); } class ProtocolError extends Error { diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts deleted file mode 100644 index 682d6f1..0000000 --- a/src/mcp/tools.ts +++ /dev/null @@ -1,658 +0,0 @@ -import type { WorkshopCapability } from '../protocol.js'; -import { KNOWLEDGE_TOOL_NAMES } from '../server/knowledge.js'; - -export interface WorkshopToolDefinition { - name: string; - title: string; - description: string; - capability: WorkshopCapability; - inputSchema: { - type: 'object'; - properties: Record; - required?: string[]; - additionalProperties: boolean; - }; -} - -const string = (description: string) => ({ type: 'string', description }); -const integer = (description: string) => ({ - type: 'integer', - minimum: 0, - description, -}); -const positiveInteger = (description: string) => ({ - type: 'integer', - minimum: 1, - description, -}); -const object = (description: string) => ({ - type: 'object', - description, - additionalProperties: true, -}); -const authoredText = (description: string) => ({ - type: 'string', - minLength: 1, - pattern: '\\S', - description, -}); -const array = (items: unknown, description: string) => ({ - type: 'array', - items, - description, -}); - -function tool( - name: string, - title: string, - description: string, - capability: WorkshopCapability, - properties: Record = {}, - required: string[] = [], - additionalProperties = false, -): WorkshopToolDefinition { - return { - name, - title, - description, - capability, - inputSchema: { - type: 'object', - properties, - ...(required.length ? { required } : {}), - additionalProperties, - }, - }; -} - -const taskFilterProperties = { - role: string('Advisory role label to match exactly.'), - state: { - type: 'string', - enum: ['unclaimed', 'claimed', 'completed', 'archived'], - description: 'Derived task state.', - }, - claimed: { type: 'boolean', description: 'Filter by current claim flag.' }, - updatedAfter: string('ISO timestamp lower bound.'), - text: string('Text to match in task description or prompt.'), - cursor: string('Opaque cursor returned by the previous page.'), - limit: integer('Page size.'), -}; - -const taskInputProperties = { - idempotencyKey: string('Stable key for safe retries.'), - description: string('Short human-readable task description.'), - role: string('Optional advisory role label; never grants authorization.'), - prompt: string('Complete instructions for the agent performing the task.'), - contextQueries: array( - { - type: 'object', - properties: { - label: string('Optional result label.'), - sparql: string( - 'Read-only SPARQL query parsed and policy-validated before save.', - ), - }, - required: ['sparql'], - additionalProperties: false, - }, - 'Queries compiled against current graph state whenever the packet is read.', - ), - memorySlugs: array( - string('Memory slug.'), - 'Existing memories to resolve into the packet.', - ), -}; - -const taskTools = [ - tool( - 'list_tasks', - 'List tasks', - 'List tasks with stable cursor pagination and optional state filters.', - 'read', - taskFilterProperties, - ), - tool( - 'search_tasks', - 'Search tasks', - 'Search existing descriptions and prompts before creating overlapping work.', - 'read', - taskFilterProperties, - ), - tool( - 'get_task_packet', - 'Get task packet', - 'Compile an ephemeral packet from the current task and current memories. Stored graph queries remain fail-closed until a scoped graph reader exists. This never claims the task.', - 'read', - { id: string('Task ID.') }, - ['id'], - ), - tool( - 'create_task', - 'Create task', - 'Validate all fields, statically validate every read-only context query, verify every memory, and atomically create a durable task.', - 'task-write', - taskInputProperties, - ['description', 'prompt'], - ), - tool( - 'update_task', - 'Update task', - 'Optimistically update an active task. Supply expectedRevision (preferred) or the legacy exact updatedAt value; completed or archived tasks cannot be reopened.', - 'task-write', - { - id: string('Task ID.'), - expectedRevision: positiveInteger('Exact current task revision.'), - expectedUpdatedAt: string('Exact current task updatedAt timestamp.'), - ...taskInputProperties, - }, - ['id'], - ), - tool( - 'archive_task', - 'Archive task', - 'Revision-conditionally archive an unclaimed task. Claimed-task archival requires an administrative HTTP integration and is not available here.', - 'task-write', - { - id: string('Task ID.'), - expectedRevision: positiveInteger('Exact current task revision.'), - expectedUpdatedAt: string('Exact current task updatedAt timestamp.'), - }, - ['id'], - ), - tool( - 'claim_task', - 'Claim task', - 'Atomically claim an unclaimed active task. Exactly one concurrent claimant can succeed.', - 'task-write', - { id: string('Task ID.') }, - ['id'], - ), - tool( - 'complete_task', - 'Complete task', - 'Complete an exactly matching claimed task with a nonempty result. Negative or inconclusive results are valid.', - 'task-write', - { id: string('Task ID.'), result: string('Nonempty task outcome.') }, - ['id', 'result'], - ), - tool( - 'task_history', - 'Task history', - 'Read bounded newest-first canonical task revision history with current authorization rechecked before content is returned.', - 'read', - { - id: string('Task ID.'), - limit: positiveInteger('Maximum revisions to return.'), - }, - ['id'], - ), -]; - -const memoryTools = [ - tool( - 'list_memories', - 'List memories', - 'List reusable operating guidance with stable pagination.', - 'read', - { - text: string('Text to search.'), - cursor: string('Opaque page cursor.'), - limit: integer('Page size.'), - }, - ), - tool( - 'get_memory', - 'Get memory', - 'Read one reusable memory by slug.', - 'read', - { slug: string('Memory slug.') }, - ['slug'], - ), - tool( - 'upsert_memory', - 'Upsert memory', - 'Create or replace reusable operating guidance. Memories should not duplicate changing graph facts.', - 'memory-write', - { - slug: string('Lowercase hyphenated slug.'), - description: string('Purpose of this guidance.'), - content: string('Reusable guidance.'), - expectedRevision: positiveInteger( - 'Optional exact current memory revision.', - ), - expectedUpdatedAt: string('Optional optimistic concurrency timestamp.'), - }, - ['slug', 'description', 'content'], - ), - tool( - 'memory_history', - 'Memory history', - 'Read bounded newest-first canonical memory revision history with current authorization rechecked before content is returned.', - 'read', - { - slug: string('Memory slug.'), - limit: positiveInteger('Maximum revisions to return.'), - }, - ['slug'], - ), -]; - -const promptTools = [ - tool( - 'list_prompts', - 'List prompts', - 'List canonical prompts with stable pagination.', - 'read', - { - text: string('Text to match.'), - active: { type: 'boolean' }, - role: string('Role.'), - scope: string('Scope.'), - cursor: string('Opaque cursor.'), - limit: integer('Page size.'), - }, - ), - tool( - 'get_prompt', - 'Get prompt', - 'Read one exact current canonical prompt.', - 'read', - { id: string('Prompt ID.') }, - ['id'], - ), - tool( - 'create_prompt', - 'Create prompt', - 'Create a revisioned canonical prompt.', - 'prompt-write', - { - id: string('Optional stable prompt ID.'), - name: string('Unique prompt name.'), - title: string('Prompt title.'), - promptText: string('Prompt instructions.'), - scope: string('Optional scope.'), - role: string('Optional role.'), - variables: object('Variable schema.'), - active: { type: 'boolean' }, - priority: integer('Priority.'), - order: integer('Stable order.'), - language: string('Language tag.'), - attribution: object('Attribution metadata.'), - }, - ['name', 'promptText'], - ), - tool( - 'update_prompt', - 'Update prompt', - 'Revision-conditionally update a canonical prompt.', - 'prompt-write', - { - id: string('Prompt ID.'), - expectedRevision: positiveInteger('Exact current revision.'), - name: string('Prompt name.'), - title: string('Prompt title.'), - promptText: string('Prompt instructions.'), - scope: string('Scope.'), - role: string('Role.'), - variables: object('Variable schema.'), - active: { type: 'boolean' }, - priority: integer('Priority.'), - order: integer('Stable order.'), - language: string('Language tag.'), - attribution: object('Attribution metadata.'), - }, - ['id', 'expectedRevision'], - ), - tool( - 'delete_prompt', - 'Delete prompt', - 'Revision-conditionally delete a canonical prompt.', - 'prompt-write', - { - id: string('Prompt ID.'), - expectedRevision: positiveInteger('Exact current revision.'), - }, - ['id', 'expectedRevision'], - ), - tool( - 'prompt_history', - 'Prompt history', - 'Read bounded newest-first canonical prompt revision history with current authorization rechecked before content is returned.', - 'read', - { - id: string('Prompt ID.'), - limit: positiveInteger('Maximum revisions to return.'), - }, - ['id'], - ), -]; - -const searchTools = [ - tool( - 'search', - 'Search', - 'Search the authorization-aware unified corpus across enabled kinds.', - 'read', - { - text: string('Search text.'), - kinds: array(string('Unified search kind.'), 'Kinds to include.'), - filters: object('Structured search filters.'), - limit: positiveInteger('Page size.'), - cursor: string('Opaque cursor.'), - }, - ['text'], - ), - tool( - 'search_status', - 'Search status', - 'Inspect materialization and semantic search status.', - 'search:admin', - ), - tool( - 'search_admin', - 'Search administration', - 'Run one exact bounded materialization, adoption, rebuild, or semantic lifecycle operation.', - 'search:admin', - { - operation: { - type: 'string', - enum: [ - 'materialize', - 'retry-dead', - 'adopt-legacy', - 'start-rebuild', - 'activate-rebuild', - 'semantic-select', - 'semantic-reconnect', - 'semantic-estimate', - 'semantic-approve', - 'semantic-run', - 'semantic-resume', - 'semantic-pause', - 'semantic-stop', - 'semantic-retry', - 'semantic-exclude', - 'semantic-retire', - 'semantic-delete-embeddings', - ], - }, - kind: { type: 'string', enum: ['task', 'memory', 'prompt'] }, - limit: positiveInteger('Bounded record count.'), - maxJobs: positiveInteger('Maximum jobs.'), - maxRebuildRoots: positiveInteger('Maximum rebuild roots.'), - configurationId: string('Configuration ID.'), - planId: string('Plan ID.'), - generation: positiveInteger('Embedding generation.'), - derivedId: string('Derived record ID.'), - reason: string('Exclusion reason.'), - policy: object('Embedding schedule policy.'), - }, - ['operation'], - ), -]; - -const knowledgeDescriptions: Record = { - search_entities: 'Search Taproot entity labels, descriptions, and aliases.', - get_entity: 'Read one canonical Taproot entity and lifecycle metadata.', - get_entities: 'Read up to 100 canonical Taproot entities.', - create_item: - 'Create one Taproot Item and return its entity and revision IDs.', - create_property: - 'Create one typed Taproot Property and return its entity and revision IDs.', - set_label: 'Set an entity label using expected-revision concurrency control.', - set_description: - 'Set an entity description using expected-revision concurrency control.', - add_alias: 'Add an entity alias using expected-revision concurrency control.', - remove_alias: - 'Remove an alias by language and ordinal using expected-revision concurrency control.', - add_sitelink: - 'Set an Item sitelink using expected-revision concurrency control.', - remove_sitelink: - 'Remove an Item sitelink using expected-revision concurrency control.', - add_statement: 'Add a complete Wikibase-shaped statement through Taproot.', - replace_statement: 'Replace one statement by ID through Taproot.', - remove_statement: 'Remove one statement by ID through Taproot.', - set_statement_rank: 'Set one statement rank through Taproot.', - add_qualifier: 'Add a typed qualifier snak to one statement through Taproot.', - remove_qualifier: - 'Remove a qualifier by property and ordinal through Taproot.', - add_reference: 'Add a complete reference to one statement through Taproot.', - remove_reference: 'Remove a statement reference by hash through Taproot.', - export_entity_json: 'Export canonical Wikibase-shaped entity JSON.', -}; - -const knowledgeReadTools = new Set([ - 'search_entities', - 'get_entity', - 'get_entities', - 'export_entity_json', -]); - -const statementSchema = { - type: 'object', - description: - 'Complete Wikibase statement with authored natural-language text for this exact revision.', - properties: { - text: authoredText( - 'Authored natural-language description of this exact statement revision.', - ), - }, - required: ['text'], - additionalProperties: true, -}; - -const claimsSchema = { - type: 'object', - description: 'Initial statements grouped by property ID.', - additionalProperties: { - type: 'array', - items: statementSchema, - }, -}; - -const knowledgeProperties: Record> = { - search_entities: { - query: string('Search text.'), - language: string('Optional language.'), - cursor: string('Opaque cursor.'), - limit: integer('Page size.'), - }, - get_entity: { entityId: string('Q or P entity ID.') }, - get_entities: { - entityIds: array(string('Q or P entity ID.'), 'Entity IDs to read.'), - }, - create_item: { - labels: object('Language label map.'), - descriptions: object('Language description map.'), - aliases: object('Language alias map.'), - claims: claimsSchema, - sitelinks: object('Initial sitelinks.'), - }, - create_property: { - datatype: string('Taproot entity datatype.'), - labels: object('Language label map.'), - descriptions: object('Language description map.'), - aliases: object('Language alias map.'), - }, - set_label: { - entityId: string('Entity ID.'), - language: string('BCP 47 language code.'), - value: string('Label.'), - expectedRevision: integer('Current revision.'), - }, - set_description: { - entityId: string('Entity ID.'), - language: string('BCP 47 language code.'), - value: string('Description.'), - expectedRevision: integer('Current revision.'), - }, - add_alias: { - entityId: string('Entity ID.'), - language: string('BCP 47 language code.'), - value: string('Alias.'), - expectedRevision: integer('Current revision.'), - }, - remove_alias: { - entityId: string('Entity ID.'), - language: string('BCP 47 language code.'), - ordinal: integer('Zero-based alias ordinal.'), - expectedRevision: integer('Current revision.'), - }, - add_sitelink: { - entityId: string('Item ID.'), - site: string('Site key.'), - sitelink: object('Wikibase sitelink.'), - expectedRevision: integer('Current revision.'), - }, - remove_sitelink: { - entityId: string('Item ID.'), - site: string('Site key.'), - expectedRevision: integer('Current revision.'), - }, - add_statement: { - entityId: string('Entity ID.'), - statement: statementSchema, - expectedRevision: integer('Current revision.'), - }, - replace_statement: { - entityId: string('Entity ID.'), - statementId: string('Statement ID.'), - statement: statementSchema, - expectedRevision: integer('Current revision.'), - }, - remove_statement: { - entityId: string('Entity ID.'), - statementId: string('Statement ID.'), - expectedRevision: integer('Current revision.'), - }, - set_statement_rank: { - entityId: string('Entity ID.'), - statementId: string('Statement ID.'), - rank: { type: 'string', enum: ['preferred', 'normal', 'deprecated'] }, - text: authoredText( - 'Explicit authored text for the ranked statement revision.', - ), - expectedRevision: integer('Current revision.'), - }, - add_qualifier: { - entityId: string('Entity ID.'), - statementId: string('Statement ID.'), - snak: object('Typed qualifier snak.'), - text: authoredText( - 'Explicit authored text for the qualified statement revision.', - ), - expectedRevision: integer('Current revision.'), - }, - remove_qualifier: { - entityId: string('Entity ID.'), - statementId: string('Statement ID.'), - property: string('Property ID.'), - ordinal: integer('Zero-based ordinal.'), - text: authoredText( - 'Explicit authored text for the statement revision after qualifier removal.', - ), - expectedRevision: integer('Current revision.'), - }, - add_reference: { - entityId: string('Entity ID.'), - statementId: string('Statement ID.'), - reference: object('Complete reference.'), - text: authoredText( - 'Explicit authored text for the referenced statement revision.', - ), - expectedRevision: integer('Current revision.'), - }, - remove_reference: { - entityId: string('Entity ID.'), - statementId: string('Statement ID.'), - hash: string('Reference hash.'), - text: authoredText( - 'Explicit authored text for the statement revision after reference removal.', - ), - expectedRevision: integer('Current revision.'), - }, - export_entity_json: { entityId: string('Entity ID.') }, -}; - -const required: Record = { - search_entities: ['query'], - get_entity: ['entityId'], - get_entities: ['entityIds'], - create_property: ['datatype'], - set_label: ['entityId', 'language', 'value', 'expectedRevision'], - set_description: ['entityId', 'language', 'value', 'expectedRevision'], - add_alias: ['entityId', 'language', 'value', 'expectedRevision'], - remove_alias: ['entityId', 'language', 'ordinal', 'expectedRevision'], - add_sitelink: ['entityId', 'site', 'sitelink', 'expectedRevision'], - remove_sitelink: ['entityId', 'site', 'expectedRevision'], - add_statement: ['entityId', 'statement', 'expectedRevision'], - replace_statement: [ - 'entityId', - 'statementId', - 'statement', - 'expectedRevision', - ], - remove_statement: ['entityId', 'statementId', 'expectedRevision'], - set_statement_rank: [ - 'entityId', - 'statementId', - 'rank', - 'text', - 'expectedRevision', - ], - add_qualifier: [ - 'entityId', - 'statementId', - 'snak', - 'text', - 'expectedRevision', - ], - remove_qualifier: [ - 'entityId', - 'statementId', - 'property', - 'ordinal', - 'text', - 'expectedRevision', - ], - add_reference: [ - 'entityId', - 'statementId', - 'reference', - 'text', - 'expectedRevision', - ], - remove_reference: [ - 'entityId', - 'statementId', - 'hash', - 'text', - 'expectedRevision', - ], - export_entity_json: ['entityId'], -}; - -const knowledgeTools = KNOWLEDGE_TOOL_NAMES.filter((name) => - knowledgeReadTools.has(name), -).map((name) => - tool( - name, - name - .split('_') - .map((part) => `${part[0]?.toUpperCase()}${part.slice(1)}`) - .join(' '), - knowledgeDescriptions[name] ?? name, - knowledgeReadTools.has(name) ? 'read' : 'knowledge-write', - knowledgeProperties[name] ?? {}, - required[name] ?? [], - ), -); - -export const workshopTools: readonly WorkshopToolDefinition[] = [ - ...taskTools, - ...memoryTools, - ...promptTools, - ...searchTools, - ...knowledgeTools, -] as const; diff --git a/src/migrations.ts b/src/migrations.ts index 98286e5..d8aa34b 100644 --- a/src/migrations.ts +++ b/src/migrations.ts @@ -12,13 +12,33 @@ export interface WorkshopMigrationResult { applied: string[]; } +export interface WorkshopMigrationPlan { + namespace: '@gnolith/workshop'; + currentVersion: number; + targetVersion: number; + ready: boolean; + pending: readonly { + id: string; + checksum: string; + statements: readonly string[]; + }[]; +} + const WORKSHOP_MIGRATION_NAMESPACE = '@gnolith/workshop'; +export type WorkshopReadOnlyPersistence = Pick; export interface WorkshopNamespacedMigration { id: string; statements: readonly string[]; } +export const workshopNamespacedMigrationsV1: readonly WorkshopNamespacedMigration[] = + Object.freeze( + workshopMigrations.map(({ id, sql }) => + Object.freeze({ id, statements: Object.freeze(splitStatements(sql)) }), + ), + ); + export interface WorkshopAppliedMigration { namespace: string; id: string; @@ -54,6 +74,52 @@ export class WorkshopMigrationError extends Error { } } +/** Read-only migration planning for an operator-owned shared DB capability. */ +export async function planWorkshopMigrations( + persistence: WorkshopPersistence, + ledgerApi: WorkshopMigrationLedgerApi, +): Promise { + const currentVersion = await installedVersion(persistence); + if (currentVersion > WORKSHOP_SCHEMA_VERSION) + throw new WorkshopMigrationError( + `Workshop schema version ${currentVersion} is newer than supported version ${WORKSHOP_SCHEMA_VERSION}`, + ); + const applied = await ledgerApi.readAppliedMigrations( + persistence, + WORKSHOP_MIGRATION_NAMESPACE, + ); + const byId = new Map(applied.map((entry) => [entry.id, entry])); + const pending: WorkshopMigrationPlan['pending'][number][] = []; + for (const migration of workshopMigrations) { + const statements = splitStatements(migration.sql); + const expectedChecksum = await ledgerApi.checksumMigration({ + id: migration.id, + statements, + }); + const recorded = byId.get(migration.id); + if (recorded && recorded.checksum !== expectedChecksum) + throw new WorkshopMigrationError( + `Checksum drift detected for ${WORKSHOP_MIGRATION_NAMESPACE}/${migration.id}`, + ); + if (!recorded) + pending.push({ + id: migration.id, + checksum: expectedChecksum, + statements, + }); + } + return { + namespace: WORKSHOP_MIGRATION_NAMESPACE, + currentVersion, + targetVersion: WORKSHOP_SCHEMA_VERSION, + ready: currentVersion === WORKSHOP_SCHEMA_VERSION && pending.length === 0, + pending, + }; +} + +/** Read-only alias used by installation diagnostics and backup validation. */ +export const inspectWorkshopMigrations = planWorkshopMigrations; + /** * Apply Workshop-owned schema migrations to any supported persistence adapter. * The host remains responsible for choosing the database and when migrations run. @@ -129,7 +195,7 @@ export async function applyWorkshopMigrations( } async function installedVersion( - persistence: WorkshopPersistence, + persistence: WorkshopReadOnlyPersistence, ): Promise { const schemaTable = await persistence .prepare( @@ -160,6 +226,21 @@ async function installedVersion( return row.version; } +/** + * Verify the exact Workshop-owned schema boundary produced by Workshop 0.4.2. + * This is read-only and deliberately does not inspect another package's tables. + */ +export async function assertWorkshopLegacyV042Schema( + persistence: WorkshopReadOnlyPersistence, +): Promise { + const version = await installedVersion(persistence); + if (version !== 6) + throw new WorkshopMigrationError( + `Workshop 0.4.2 legacy source must attest schema version 6; found ${version}`, + ); + await validateLegacySchema(persistence, version); +} + function packageVersionForSchema(version: number): string | undefined { switch (version) { case 1: @@ -172,6 +253,12 @@ function packageVersionForSchema(version: number): string | undefined { return '0.3.0'; case 6: return '0.4.0'; + case 7: + case 8: + case 9: + case 10: + case 11: + return '0.5.0'; default: return undefined; } @@ -182,18 +269,18 @@ async function loadDiamondMigrationLedger(): Promise return await import('@gnolith/diamond'); } catch (cause) { throw new WorkshopMigrationError( - 'Applying Workshop migrations requires the @gnolith/diamond 0.4 migration ledger API', + 'Applying Workshop migrations requires the @gnolith/diamond 0.5 migration ledger API', { cause }, ); } } async function validateLegacySchema( - persistence: WorkshopPersistence, + persistence: WorkshopReadOnlyPersistence, installed: number, ): Promise { - if (installed === 6) { - await validateCanonicalSearchSchema(persistence); + if (installed >= 6) { + await validateCanonicalSearchSchema(persistence, installed); return; } const expected = legacySchema(installed); @@ -339,7 +426,8 @@ async function validateLegacySchema( } async function validateCanonicalSearchSchema( - persistence: WorkshopPersistence, + persistence: WorkshopReadOnlyPersistence, + installed: number, ): Promise { const tables = await persistence .prepare( @@ -347,21 +435,43 @@ async function validateCanonicalSearchSchema( ) .all<{ name: string; sql: string }>(); const expectedTables = [ + ...(installed >= 11 + ? [ + 'workshop_authorization_admin_receipts', + 'workshop_authorization_audit', + 'workshop_authorization_principals', + ] + : []), + ...(installed >= 8 + ? ['workshop_bootstrap_receipts', 'workshop_credentials'] + : []), 'workshop_cursor_entries', 'workshop_cursor_snapshots', 'workshop_memories', 'workshop_memory_revisions', + ...(installed >= 11 ? ['workshop_oauth_bindings'] : []), 'workshop_onboarding_runs', 'workshop_onboarding_steps', + ...(installed >= 9 ? ['workshop_operation_receipts'] : []), 'workshop_prompt_revisions', 'workshop_prompts', 'workshop_schema', + ...(installed >= 11 + ? [ + 'workshop_semantic_configuration_receipts', + 'workshop_semantic_configuration_state', + ] + : []), + ...(installed >= 7 + ? ['workshop_task_lease_events', 'workshop_task_lease_receipts'] + : []), 'workshop_task_revisions', 'workshop_tasks', + ...(installed >= 11 ? ['workshop_workspace_memberships'] : []), ]; const actualTables = tables.results.map(({ name }) => name); if (JSON.stringify(actualTables) !== JSON.stringify(expectedTables)) - throw adoptionError(installedVersionNumber(), 'table set does not match'); + throw adoptionError(installed, 'table set does not match'); const requiredColumns: Record = { workshop_tasks: [ @@ -377,6 +487,14 @@ async function validateCanonicalSearchSchema( 'policy_revision', 'search_event_id', 'search_event_sequence', + ...(installed >= 7 + ? [ + 'lease_id', + 'lease_token_digest', + 'lease_owner_principal_id', + 'lease_expires_at', + ] + : []), ], workshop_memories: [ 'title', @@ -423,10 +541,7 @@ async function validateCanonicalSearchSchema( .all(); const names = new Set(columns.results.map(({ name }) => name)); if (required.some((name) => !names.has(name))) - throw adoptionError( - installedVersionNumber(), - `${table} columns do not match`, - ); + throw adoptionError(installed, `${table} columns do not match`); } const indexes = await persistence @@ -435,18 +550,26 @@ async function validateCanonicalSearchSchema( ) .all<{ name: string }>(); const requiredIndexes = [ + ...(installed >= 11 + ? ['workshop_authorization_audit_installation_idx'] + : []), + ...(installed >= 8 ? ['workshop_credentials_principal_idx'] : []), 'workshop_cursor_snapshots_expiry_idx', 'workshop_memories_authorization_idx', 'workshop_memories_idempotency_idx', 'workshop_memories_updated_idx', 'workshop_memory_revisions_history_idx', + ...(installed >= 11 ? ['workshop_oauth_bindings_principal_idx'] : []), 'workshop_onboarding_runs_state_idx', 'workshop_onboarding_steps_state_idx', + ...(installed >= 9 ? ['workshop_operation_receipts_subject_idx'] : []), 'workshop_prompt_revisions_history_idx', 'workshop_prompts_authorization_idx', 'workshop_prompts_list_idx', + ...(installed >= 7 ? ['workshop_task_lease_events_task_idx'] : []), 'workshop_task_revisions_history_idx', 'workshop_tasks_authorization_idx', + ...(installed >= 7 ? ['workshop_tasks_lease_expiry_idx'] : []), 'workshop_tasks_role_idx', 'workshop_tasks_state_idx', 'workshop_tasks_updated_idx', @@ -455,11 +578,7 @@ async function validateCanonicalSearchSchema( JSON.stringify(indexes.results.map(({ name }) => name)) !== JSON.stringify(requiredIndexes) ) - throw adoptionError(installedVersionNumber(), 'index set does not match'); -} - -function installedVersionNumber(): number { - return 6; + throw adoptionError(installed, 'index set does not match'); } interface ColumnInfo { diff --git a/src/migrations/manifest.ts b/src/migrations/manifest.ts index 8be179e..8e29d03 100644 --- a/src/migrations/manifest.ts +++ b/src/migrations/manifest.ts @@ -1,10 +1,10 @@ export interface WorkshopMigration { id: string; - checksum: string; + checksum: `sha256:${string}`; sql: string; } -export const WORKSHOP_SCHEMA_VERSION = 6; +export const WORKSHOP_SCHEMA_VERSION = 11; export const workshopMigrations: readonly WorkshopMigration[] = [ { @@ -428,36 +428,382 @@ UPDATE workshop_schema SET version = 6, package_version = '0.4.0', updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') +WHERE singleton = 1;`, + }, + { + id: '0007_task_leases', + checksum: + 'sha256:c617200d9c0b0cc629e738937b7a08ed2e1a29a6be3d99a1835d6bb2b47a1b69', + sql: `ALTER TABLE workshop_tasks ADD COLUMN lease_id TEXT; +ALTER TABLE workshop_tasks ADD COLUMN lease_token_digest TEXT; +ALTER TABLE workshop_tasks ADD COLUMN lease_owner_principal_id TEXT; +ALTER TABLE workshop_tasks ADD COLUMN lease_expires_at TEXT; + +CREATE TABLE workshop_task_lease_receipts ( + installation_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + idempotency_key TEXT NOT NULL, + task_id TEXT NOT NULL, + operation TEXT NOT NULL CHECK (operation IN ('release', 'complete')), + expected_revision INTEGER NOT NULL CHECK (expected_revision >= 1), + resulting_revision INTEGER NOT NULL CHECK (resulting_revision >= 1), + receipt_json TEXT NOT NULL CHECK (json_valid(receipt_json)), + created_at TEXT NOT NULL, + PRIMARY KEY (installation_id, principal_id, idempotency_key) +); + +CREATE TABLE workshop_task_lease_events ( + event_id TEXT PRIMARY KEY, + installation_id TEXT NOT NULL, + task_id TEXT NOT NULL, + lease_id TEXT NOT NULL, + actor_principal_id TEXT NOT NULL, + event_kind TEXT NOT NULL CHECK ( + event_kind IN ('claimed', 'reclaimed_expired', 'released', 'completed') + ), + task_revision INTEGER NOT NULL CHECK (task_revision >= 1), + lease_expires_at TEXT, + created_at TEXT NOT NULL +); + +CREATE INDEX workshop_tasks_lease_expiry_idx + ON workshop_tasks (installation_id, lease_expires_at, id); +CREATE INDEX workshop_task_lease_events_task_idx + ON workshop_task_lease_events (installation_id, task_id, created_at); + +UPDATE workshop_schema +SET version = 7, + package_version = '0.5.0', + updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') +WHERE singleton = 1;`, + }, + { + id: '0008_bootstrap_credentials', + checksum: + 'sha256:319aacd693f45d8b906fd25932f75253629ebe58fc0037f0e341be7baafd31a3', + sql: `CREATE TABLE workshop_credentials ( + installation_id TEXT NOT NULL, + credential_id TEXT NOT NULL, + profile TEXT NOT NULL CHECK (profile = 'local-bearer-v1'), + token_digest TEXT NOT NULL CHECK ( + length(token_digest) = 64 AND token_digest NOT GLOB '*[^0-9a-f]*' + ), + principal_id TEXT NOT NULL, + workspace_id TEXT NOT NULL, + disabled INTEGER NOT NULL DEFAULT 0 CHECK (disabled IN (0, 1)), + created_at TEXT NOT NULL, + PRIMARY KEY (installation_id, credential_id), + UNIQUE (installation_id, token_digest) +); + +CREATE INDEX workshop_credentials_principal_idx + ON workshop_credentials (installation_id, principal_id, workspace_id); + +CREATE TABLE workshop_bootstrap_receipts ( + installation_id TEXT NOT NULL, + idempotency_key TEXT NOT NULL, + credential_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + workspace_id TEXT NOT NULL, + grant_manifest_digest TEXT NOT NULL CHECK ( + length(grant_manifest_digest) = 64 + AND grant_manifest_digest NOT GLOB '*[^0-9a-f]*' + ), + owner_receipt_json TEXT NOT NULL CHECK (json_valid(owner_receipt_json)), + created_at TEXT NOT NULL, + PRIMARY KEY (installation_id, idempotency_key), + FOREIGN KEY (installation_id, credential_id) + REFERENCES workshop_credentials(installation_id, credential_id) +); + +UPDATE workshop_schema +SET version = 8, + package_version = '0.5.0', + updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') +WHERE singleton = 1;`, + }, + { + id: '0009_operation_receipts', + checksum: + 'sha256:a5b953d80f796cb16c5de532283b8f16f34a2f3de56bb721e4e07f8811ed657c', + sql: `CREATE TABLE workshop_operation_receipts ( + installation_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + operation TEXT NOT NULL CHECK ( + operation IN ( + 'memory.delete', + 'prompt.create', + 'prompt.update', + 'prompt.delete' + ) + ), + idempotency_key TEXT NOT NULL, + subject_id TEXT NOT NULL, + request_digest TEXT NOT NULL CHECK ( + length(request_digest) = 64 + AND request_digest NOT GLOB '*[^0-9a-f]*' + ), + response_json TEXT NOT NULL CHECK (json_valid(response_json)), + created_at TEXT NOT NULL, + PRIMARY KEY ( + installation_id, + principal_id, + operation, + idempotency_key + ) +); + +CREATE INDEX workshop_operation_receipts_subject_idx + ON workshop_operation_receipts ( + installation_id, + operation, + subject_id, + created_at + ); + +UPDATE workshop_schema +SET version = 9, + package_version = '0.5.0', + updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') +WHERE singleton = 1;`, + }, + { + id: '0010_durable_mutation_receipts', + checksum: + 'sha256:d858a4401f715cc954f16a23d2a8e332c55da5f0eb06c4b3a9bf3acdfad56421', + sql: `ALTER TABLE workshop_task_lease_receipts +ADD COLUMN request_digest TEXT CHECK ( + request_digest IS NULL OR ( + length(request_digest) = 64 + AND request_digest NOT GLOB '*[^0-9a-f]*' + ) +); + +ALTER TABLE workshop_operation_receipts +RENAME TO workshop_operation_receipts_v9; + +CREATE TABLE workshop_operation_receipts ( + installation_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + operation TEXT NOT NULL CHECK ( + operation IN ( + 'memory.upsert', + 'memory.delete', + 'prompt.create', + 'prompt.update', + 'prompt.delete' + ) + ), + idempotency_key TEXT NOT NULL, + subject_id TEXT NOT NULL, + request_digest TEXT NOT NULL CHECK ( + length(request_digest) = 64 + AND request_digest NOT GLOB '*[^0-9a-f]*' + ), + response_json TEXT NOT NULL CHECK (json_valid(response_json)), + created_at TEXT NOT NULL, + PRIMARY KEY ( + installation_id, + principal_id, + operation, + idempotency_key + ) +); + +INSERT INTO workshop_operation_receipts ( + installation_id, + principal_id, + operation, + idempotency_key, + subject_id, + request_digest, + response_json, + created_at +) +SELECT + installation_id, + principal_id, + operation, + idempotency_key, + subject_id, + request_digest, + response_json, + created_at +FROM workshop_operation_receipts_v9; + +DROP TABLE workshop_operation_receipts_v9; + +CREATE INDEX workshop_operation_receipts_subject_idx + ON workshop_operation_receipts ( + installation_id, + operation, + subject_id, + created_at + ); + +UPDATE workshop_schema +SET version = 10, + package_version = '0.5.0', + updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') +WHERE singleton = 1;`, + }, + { + id: '0011_administration_control_plane', + checksum: + 'sha256:603c51fa50d12f45c23ca41e3c7cc765968b1e64499e89530e98335ccc5e417d', + sql: `CREATE TABLE workshop_authorization_principals ( + installation_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + lifecycle TEXT NOT NULL CHECK ( + lifecycle IN ('active', 'disabled', 'revoked') + ), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + PRIMARY KEY (installation_id, principal_id) +); + +CREATE TABLE workshop_workspace_memberships ( + installation_id TEXT NOT NULL, + principal_id TEXT NOT NULL, + workspace_id TEXT NOT NULL, + active INTEGER NOT NULL CHECK (active IN (0, 1)), + updated_at TEXT NOT NULL, + PRIMARY KEY (installation_id, principal_id, workspace_id) +); + +CREATE TABLE workshop_oauth_bindings ( + installation_id TEXT NOT NULL, + issuer TEXT NOT NULL, + subject TEXT NOT NULL, + principal_id TEXT NOT NULL, + workspace_id TEXT NOT NULL, + credential_id TEXT NOT NULL, + active INTEGER NOT NULL CHECK (active IN (0, 1)), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + PRIMARY KEY (installation_id, issuer, subject) +); + +CREATE INDEX workshop_oauth_bindings_principal_idx + ON workshop_oauth_bindings ( + installation_id, + principal_id, + workspace_id, + active + ); + +CREATE TABLE workshop_authorization_admin_receipts ( + installation_id TEXT NOT NULL, + actor_principal_id TEXT NOT NULL, + idempotency_key TEXT NOT NULL, + action TEXT NOT NULL, + request_digest TEXT NOT NULL CHECK ( + length(request_digest) = 64 + AND request_digest NOT GLOB '*[^0-9a-f]*' + ), + response_json TEXT NOT NULL CHECK (json_valid(response_json)), + created_at TEXT NOT NULL, + PRIMARY KEY (installation_id, actor_principal_id, idempotency_key) +); + +CREATE TABLE workshop_authorization_audit ( + audit_id TEXT PRIMARY KEY, + installation_id TEXT NOT NULL, + actor_principal_id TEXT NOT NULL, + action TEXT NOT NULL, + target_kind TEXT NOT NULL, + target_id TEXT NOT NULL, + authorization_revision INTEGER NOT NULL CHECK ( + authorization_revision >= 1 + ), + details_json TEXT NOT NULL CHECK (json_valid(details_json)), + created_at TEXT NOT NULL +); + +CREATE INDEX workshop_authorization_audit_installation_idx + ON workshop_authorization_audit ( + installation_id, + created_at DESC, + audit_id + ); + +CREATE TABLE workshop_semantic_configuration_state ( + installation_id TEXT PRIMARY KEY, + revision INTEGER NOT NULL CHECK (revision >= 1), + fingerprint TEXT NOT NULL CHECK ( + length(fingerprint) = 64 + AND fingerprint NOT GLOB '*[^0-9a-f]*' + ), + configuration_json TEXT NOT NULL CHECK (json_valid(configuration_json)), + ready INTEGER NOT NULL CHECK (ready IN (0, 1)), + updated_at TEXT NOT NULL +); + +CREATE TABLE workshop_semantic_configuration_receipts ( + installation_id TEXT NOT NULL, + actor_principal_id TEXT NOT NULL, + idempotency_key TEXT NOT NULL, + request_digest TEXT NOT NULL CHECK ( + length(request_digest) = 64 + AND request_digest NOT GLOB '*[^0-9a-f]*' + ), + response_json TEXT NOT NULL CHECK (json_valid(response_json)), + created_at TEXT NOT NULL, + PRIMARY KEY (installation_id, actor_principal_id, idempotency_key) +); + +UPDATE workshop_schema +SET version = 11, + package_version = '0.5.0', + updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE singleton = 1;`, }, ] as const; export const expectedWorkshopTables = [ + 'workshop_authorization_admin_receipts', + 'workshop_authorization_audit', + 'workshop_authorization_principals', 'workshop_cursor_entries', 'workshop_cursor_snapshots', + 'workshop_bootstrap_receipts', + 'workshop_credentials', 'workshop_memory_revisions', 'workshop_memories', 'workshop_onboarding_runs', 'workshop_onboarding_steps', + 'workshop_oauth_bindings', + 'workshop_operation_receipts', 'workshop_prompt_revisions', 'workshop_prompts', 'workshop_schema', + 'workshop_semantic_configuration_receipts', + 'workshop_semantic_configuration_state', 'workshop_task_revisions', + 'workshop_task_lease_events', + 'workshop_task_lease_receipts', 'workshop_tasks', + 'workshop_workspace_memberships', ] as const; export const expectedWorkshopIndexes = [ + 'workshop_authorization_audit_installation_idx', 'workshop_cursor_snapshots_expiry_idx', + 'workshop_credentials_principal_idx', 'workshop_memory_revisions_history_idx', 'workshop_memories_authorization_idx', 'workshop_memories_idempotency_idx', 'workshop_memories_updated_idx', 'workshop_onboarding_runs_state_idx', 'workshop_onboarding_steps_state_idx', + 'workshop_oauth_bindings_principal_idx', + 'workshop_operation_receipts_subject_idx', 'workshop_prompt_revisions_history_idx', 'workshop_prompts_authorization_idx', 'workshop_prompts_list_idx', 'workshop_task_revisions_history_idx', + 'workshop_task_lease_events_task_idx', + 'workshop_tasks_lease_expiry_idx', 'workshop_tasks_role_idx', 'workshop_tasks_authorization_idx', 'workshop_tasks_state_idx', diff --git a/src/operations.ts b/src/operations.ts new file mode 100644 index 0000000..5963e0b --- /dev/null +++ b/src/operations.ts @@ -0,0 +1,383 @@ +import Ajv, { type ValidateFunction } from 'ajv'; +import { + CATALOG_DIGEST, + OPERATION_SCHEMA_VERSION, + operationCatalog, + type OperationInput, + type OperationName, + type OperationOutput, +} from './generated/operations.js'; +import { WorkshopError, normalizeWorkshopError } from './protocol/errors.js'; +import { + assertSearchResultIdentity, + type WorkshopSearchResultIdentity, +} from './protocol/catalog.js'; +import type { AuthenticatedPrincipal, WorkshopCapability } from './protocol.js'; + +const IDENTITY_ARGUMENTS = new Set([ + 'installationId', + 'principalId', + 'activeWorkspaceId', + 'workspaceIds', + 'capabilities', + 'authorizationRevision', + 'credentialId', + 'actorPrincipalId', +]); + +export interface OperationCallContext { + principal: AuthenticatedPrincipal; + requestId: string; + signal: AbortSignal; +} + +export interface WorkshopStatusSnapshot { + baseIri: string; + migrations: { + namespace: '@gnolith/workshop'; + version: number; + ready: boolean; + }; + compatibility: { diamond: boolean; taproot: boolean }; + canonicalReady: boolean; + authorizationReady: boolean; + lexicalReady: boolean; + semantic: { + state: 'ready' | 'degraded' | 'unconfigured'; + configured: boolean; + revision: number | null; + fingerprint: string | null; + ready: boolean; + diagnostic: { + format: 'gnolith-workshop-semantic-diagnostic-v1'; + version: 1; + code: 'materialization-pending' | 'provider-unavailable'; + retryable: boolean; + backend: 'sqlite' | 'qdrant'; + repair: 'retry'; + } | null; + }; + producers: { + ready: boolean; + fingerprint: string; + kinds: readonly ['task', 'memory', 'prompt']; + }; + blobReady: boolean; +} + +export interface TaprootOperationPort { + call( + name: OperationName, + input: Readonly>, + context: OperationCallContext, + ): Promise; + createScopedSparqlView(context: OperationCallContext): Promise; + recheckPrincipal( + principal: AuthenticatedPrincipal, + signal: AbortSignal, + ): Promise; + status( + principal: AuthenticatedPrincipal, + signal: AbortSignal, + ): Promise; +} + +export interface DiamondSparqlPort { + validate(query: string, options: { signal: AbortSignal }): Promise; + query( + scopedView: unknown, + query: string, + options: { + dryRun: boolean; + limit?: number; + signal: AbortSignal; + }, + ): Promise; + close?(): void | Promise; +} + +export interface WorkshopDomainOperationPort { + call( + name: OperationName, + input: Readonly>, + context: OperationCallContext, + ): Promise; +} + +export interface OperationObservation { + operation: OperationName; + operationVersion: 1; + requestId: string; + principalId: string; + outcome: 'success' | 'error'; + durationMs: number; + errorCode?: string; +} + +export interface OperationRuntimeOptions { + version: string; + taproot: TaprootOperationPort; + diamond: DiamondSparqlPort; + workshop: WorkshopDomainOperationPort; + observe?: (event: OperationObservation) => void | Promise; +} + +export interface OperationRuntime { + readonly catalog: typeof operationCatalog; + readonly catalogDigest: typeof CATALOG_DIGEST; + call( + name: Name, + input: OperationInput, + context: OperationCallContext, + ): Promise>; +} + +const TAPROOT_NAMES = new Set([ + 'authorization_admin', + 'search', + 'search_hydrate', + 'search_admin', + 'get_entity', + 'get_entities', + 'create_item', + 'create_property', + 'mutate_entity', + 'entity_history', + 'entity_revision', + 'statement_history', + 'statement_revision', + 'export_entity_json', + 'create_resource', + 'get_resource', + 'update_resource', + 'delete_resource', + 'hydrate_resource', + 'resource_history', + 'resource_revision', + 'create_annotation', + 'get_annotation', + 'update_annotation', + 'delete_annotation', + 'annotation_history', + 'annotation_revision', +]); +const WORKSHOP_NAMES = new Set([ + 'list_tasks', + 'search_tasks', + 'get_task', + 'get_task_packet', + 'create_task', + 'update_task', + 'archive_task', + 'claim_task', + 'release_task', + 'complete_task', + 'task_history', + 'list_memories', + 'get_memory', + 'upsert_memory', + 'delete_memory', + 'memory_history', + 'list_prompts', + 'get_prompt', + 'create_prompt', + 'update_prompt', + 'delete_prompt', + 'prompt_history', +]); + +export function createOperationRuntime( + options: OperationRuntimeOptions, +): OperationRuntime { + const validators = validatorsForCatalog(); + const definitions = new Map( + operationCatalog.map((definition) => [definition.name, definition]), + ); + return { + catalog: operationCatalog, + catalogDigest: CATALOG_DIGEST, + async call( + name: Name, + input: OperationInput, + context: OperationCallContext, + ): Promise> { + const started = performance.now(); + let live: AuthenticatedPrincipal | undefined; + try { + const definition = definitions.get(name); + if (!definition) + throw new WorkshopError('not_found', 'Operation not found', 404); + if (!input || typeof input !== 'object' || Array.isArray(input)) + throw invalid('Operation input must be an object'); + const validate = validators.get(name); + if (!validate?.(input)) + throw new WorkshopError( + 'validation_failed', + 'Operation input does not match its schema', + 400, + { operation: name, errors: boundedAjvErrors(validate?.errors) }, + ); + rejectForgedIdentity(name, input); + live = await options.taproot.recheckPrincipal( + context.principal, + context.signal, + ); + requireExactCapability(live.capabilities, definition.capability); + const liveContext = { ...context, principal: live }; + let value: unknown; + if (name === 'gnolith_status') { + const status = await options.taproot.status(live, context.signal); + value = { + installationId: live.installationId, + baseIri: status.baseIri, + principalId: live.principalId, + credentialId: live.credentialId, + activeWorkspaceId: live.activeWorkspaceId, + capabilities: [...live.capabilities], + authorizationRevision: live.authorizationRevision, + migrationReadiness: status.migrations, + compatibility: status.compatibility, + canonicalReady: status.canonicalReady, + authorizationReady: status.authorizationReady, + lexicalReady: status.lexicalReady, + semanticState: status.semantic, + producers: status.producers, + blobReady: status.blobReady, + versions: { + server: options.version, + operationSchema: OPERATION_SCHEMA_VERSION, + }, + operationCatalogDigest: CATALOG_DIGEST, + }; + } else if (name === 'validate_sparql') { + value = await options.diamond.validate(String(input.query), { + signal: context.signal, + }); + } else if (name === 'query_sparql') { + const scopedView = + await options.taproot.createScopedSparqlView(liveContext); + value = await options.diamond.query(scopedView, String(input.query), { + dryRun: input.dryRun === true, + ...(typeof input.limit === 'number' ? { limit: input.limit } : {}), + signal: context.signal, + }); + } else if (TAPROOT_NAMES.has(name)) { + value = await options.taproot.call(name, input, liveContext); + } else if (WORKSHOP_NAMES.has(name)) { + value = await options.workshop.call(name, input, liveContext); + } else { + throw new WorkshopError( + 'internal_error', + 'Operation is not bound', + 500, + ); + } + if (name === 'search') validateSearchPage(value); + const outputValidator = validators.get(`${name}:output`); + if (!outputValidator?.(value)) + throw new WorkshopError( + 'internal_error', + 'Operation output does not match its schema', + 500, + { + operation: name, + errors: boundedAjvErrors(outputValidator?.errors), + }, + ); + await observe(options, { + operation: name, + operationVersion: 1, + requestId: context.requestId, + principalId: live.principalId, + outcome: 'success', + durationMs: performance.now() - started, + }); + return value as OperationOutput; + } catch (error) { + const normalized = normalizeWorkshopError(error); + await observe(options, { + operation: name, + operationVersion: 1, + requestId: context.requestId, + principalId: live?.principalId ?? context.principal.principalId, + outcome: 'error', + durationMs: performance.now() - started, + errorCode: normalized.code, + }); + throw normalized; + } + }, + }; +} + +function validatorsForCatalog(): Map { + const AjvConstructor = Ajv as unknown as new (options: { + allErrors: boolean; + strict: boolean; + }) => { + compile(schema: unknown): ValidateFunction; + }; + const ajv = new AjvConstructor({ allErrors: true, strict: true }); + const validators = new Map(); + for (const operation of operationCatalog) { + validators.set(operation.name, ajv.compile(operation.inputSchema)); + validators.set( + `${operation.name}:output`, + ajv.compile(operation.outputSchema), + ); + } + return validators; +} + +function rejectForgedIdentity( + name: OperationName, + input: Readonly>, +): void { + // Authorization administration targets are part of this operation's sealed, + // schema-validated command. They identify the object being administered and + // never replace the authenticated caller in OperationCallContext. + if (name === 'authorization_admin') return; + for (const key of Object.keys(input)) { + if (IDENTITY_ARGUMENTS.has(key)) + throw invalid('Identity and authorization are transport-owned'); + } +} + +function requireExactCapability( + capabilities: readonly string[], + required: WorkshopCapability, +): void { + if (!capabilities.includes(required)) + throw new WorkshopError('forbidden', 'Authorization denied', 403); +} + +function validateSearchPage(value: unknown): void { + if (!value || typeof value !== 'object') return; + const items = (value as { items?: unknown }).items; + if (!Array.isArray(items)) return; + for (const item of items) + assertSearchResultIdentity(item as WorkshopSearchResultIdentity); +} + +function boundedAjvErrors(errors: ValidateFunction['errors']): unknown[] { + return (errors ?? []).slice(0, 20).map((error) => ({ + instancePath: error.instancePath.slice(0, 256), + keyword: error.keyword.slice(0, 64), + message: error.message?.slice(0, 256), + })); +} + +function invalid(message: string): WorkshopError { + return new WorkshopError('validation_failed', message, 400); +} + +async function observe( + options: OperationRuntimeOptions, + event: OperationObservation, +): Promise { + try { + await options.observe?.(event); + } catch { + // Observability never changes operation outcomes. + } +} diff --git a/src/producers.ts b/src/producers.ts new file mode 100644 index 0000000..392ab2f --- /dev/null +++ b/src/producers.ts @@ -0,0 +1,111 @@ +import { WorkshopError } from './protocol/errors.js'; + +export const WORKSHOP_PRODUCER_KINDS = ['task', 'memory', 'prompt'] as const; +export type WorkshopProducerKind = (typeof WORKSHOP_PRODUCER_KINDS)[number]; + +export interface PublicCanonicalProducerHandle { + readonly owner: '@gnolith/workshop'; + readonly installationId: string; + readonly connectionId: string; + readonly registrationFingerprint: string; + readonly handle: unknown; +} + +const VERIFIED_PRODUCERS: unique symbol = Symbol('verified producers'); + +export interface VerifiedWorkshopProducerBinding { + readonly [VERIFIED_PRODUCERS]: true; + readonly installationId: string; + readonly connectionId: string; + readonly registrationFingerprint: string; + readonly handles: Readonly>; +} + +export interface WorkshopProducerBindingExpectation { + installationId: string; + connectionId: string; + registrationFingerprint: string; +} + +const verifiedBindings = new WeakSet(); + +export function bindWorkshopProducerHandles( + expectation: WorkshopProducerBindingExpectation, + mappings: Readonly< + Record + >, +): VerifiedWorkshopProducerBinding { + validateIdentifier(expectation.installationId, 'installationId'); + validateIdentifier(expectation.connectionId, 'connectionId'); + validateFingerprint(expectation.registrationFingerprint); + if ( + Object.keys(mappings).sort().join(',') !== + [...WORKSHOP_PRODUCER_KINDS].sort().join(',') + ) + throw invalid('Exactly three canonical producer mappings are required'); + const byKind = new Map(); + const opaqueHandles = new Set(); + for (const publicKind of WORKSHOP_PRODUCER_KINDS) { + const producer = mappings[publicKind]; + if ( + producer.owner !== '@gnolith/workshop' || + producer.installationId !== expectation.installationId || + producer.connectionId !== expectation.connectionId || + producer.registrationFingerprint !== + expectation.registrationFingerprint || + producer.handle === null || + producer.handle === undefined || + opaqueHandles.has(producer.handle) + ) + throw invalid('Canonical producer handle binding is invalid'); + opaqueHandles.add(producer.handle); + byKind.set(publicKind, producer.handle); + } + const binding = Object.freeze({ + [VERIFIED_PRODUCERS]: true as const, + installationId: expectation.installationId, + connectionId: expectation.connectionId, + registrationFingerprint: expectation.registrationFingerprint, + handles: Object.freeze({ + task: byKind.get('task')!, + memory: byKind.get('memory')!, + prompt: byKind.get('prompt')!, + }), + }); + verifiedBindings.add(binding); + return binding; +} + +export function assertVerifiedWorkshopProducerBinding( + binding: VerifiedWorkshopProducerBinding, + expectation: WorkshopProducerBindingExpectation, +): void { + if ( + !verifiedBindings.has(binding) || + binding.installationId !== expectation.installationId || + binding.connectionId !== expectation.connectionId || + binding.registrationFingerprint !== expectation.registrationFingerprint || + Object.keys(binding.handles).sort().join(',') !== + [...WORKSHOP_PRODUCER_KINDS].sort().join(',') + ) + throw invalid('Canonical producer binding is not verified'); +} + +function validateIdentifier(value: unknown, field: string): void { + if ( + typeof value !== 'string' || + !value || + value.length > 256 || + value.includes('\u0000') + ) + throw invalid(`${field} is invalid`); +} + +function validateFingerprint(value: unknown): void { + if (typeof value !== 'string' || !/^[a-f0-9]{64}$/u.test(value)) + throw invalid('registrationFingerprint is invalid'); +} + +function invalid(message: string): WorkshopError { + return new WorkshopError('validation_failed', message, 400); +} diff --git a/src/protocol.ts b/src/protocol.ts index 45321de..300e561 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -4,20 +4,39 @@ export * from './protocol/memories.js'; export * from './protocol/prompts.js'; export * from './protocol/knowledge.js'; export * from './protocol/client.js'; +export * from './protocol/catalog.js'; export * from './protocol/authorization.js'; import type { AuthorizationContext } from './protocol/authorization.js'; export type WorkshopCapability = | 'read' - | 'task-write' - | 'knowledge-write' - | 'memory-write' - | 'prompt-write' + | 'task:write' + | 'knowledge:write' + | 'knowledge:policy' + | 'memory:write' + | 'prompt:write' | 'admin' | 'search:admin'; -/** @deprecated Name retained for transport resolver compatibility. */ -export type WorkshopPrincipal = AuthorizationContext; +export interface AuthenticatedPrincipal { + installationId: string; + principalId: string; + activeWorkspaceId: string | null; + /** + * Populated only after Taproot's live authorization recheck. Transport + * authentication alone does not establish workspace membership. + */ + workspaceIds?: readonly string[]; + capabilities: readonly string[]; + authorizationRevision: number; + credentialId: string; +} + +/** Internal authorization context after the live server-side membership fence. */ +export interface WorkshopPrincipal extends AuthorizationContext { + /** Present on transport-authenticated principals; legacy process-local callers omit it. */ + credentialId?: string; +} export function hasWorkshopCapability( capabilities: readonly string[], diff --git a/src/protocol/authorization.ts b/src/protocol/authorization.ts index 4e8b864..a49d712 100644 --- a/src/protocol/authorization.ts +++ b/src/protocol/authorization.ts @@ -1,4 +1,4 @@ -/** Host-authenticated identity. This is the exact Taproot 0.3 authorization shape. */ +/** Host-authenticated identity supplied through Taproot's public authorization port. */ export interface AuthorizationContext { installationId: string; principalId: string; diff --git a/src/protocol/catalog.ts b/src/protocol/catalog.ts new file mode 100644 index 0000000..a5acd1b --- /dev/null +++ b/src/protocol/catalog.ts @@ -0,0 +1,41 @@ +export { + CATALOG_DIGEST, + OPERATION_ROUTE_PREFIX, + OPERATION_SCHEMA_VERSION, + legacyOperationNames, + operationCatalog, + type OperationInput, + type OperationName, + type OperationOutput, +} from '../generated/operations.js'; + +export const WORKSHOP_SEARCH_KINDS = [ + 'statement', + 'item', + 'task', + 'memory', + 'prompt', + 'resource', + 'annotation', +] as const; + +export type WorkshopSearchKind = (typeof WORKSHOP_SEARCH_KINDS)[number]; +export type WorkshopEntityType = 'item' | 'property'; + +export interface WorkshopSearchResultIdentity { + kind: WorkshopSearchKind; + id: string; + revision: number; + entityType?: WorkshopEntityType; +} + +export function assertSearchResultIdentity( + value: WorkshopSearchResultIdentity, +): void { + if (value.kind === 'item') { + if (value.entityType !== 'item' && value.entityType !== 'property') + throw new TypeError('item search results require entityType'); + } else if (value.entityType !== undefined) { + throw new TypeError('entityType is only valid for item search results'); + } +} diff --git a/src/protocol/client.ts b/src/protocol/client.ts index 661c03a..243b44a 100644 --- a/src/protocol/client.ts +++ b/src/protocol/client.ts @@ -1,319 +1,116 @@ +import { + OPERATION_ROUTE_PREFIX, + type OperationInput, + type OperationName, + type OperationOutput, +} from '../generated/operations.js'; import { WorkshopError } from './errors.js'; -import type { - MemoryFilters, - MemoryPage, - UpsertMemoryInput, -} from './memories.js'; -import type { - CreateTaskInput, - Task, - TaskFilters, - TaskPacket, - TaskPage, - RevisionHistoryOptions, - UpdateTaskInput, -} from './tasks.js'; -import type { Memory } from './memories.js'; -import type { MemoryRevision } from './memories.js'; -import type { TaskRevision } from './tasks.js'; -import type { - CreatePromptInput, - Prompt, - PromptFilters, - PromptPage, - PromptRevision, - UpdatePromptInput, -} from './prompts.js'; -import type { SearchPage, SearchRequest } from '@gnolith/taproot'; export interface WorkshopClientOptions { - baseUrl?: string; + baseOrigin?: string; fetch?: typeof globalThis.fetch; - token?: () => string | null | Promise; + accessToken?: () => string | null | Promise; } -export interface RequestOptions { +export interface WorkshopCallOptions { signal?: AbortSignal; } -export interface HistoryRequestOptions - extends RequestOptions, RevisionHistoryOptions {} - -interface WorkshopRequestInit extends Omit { - signal?: AbortSignal | undefined; -} - export interface WorkshopClient { - tasks: { - list(filters?: TaskFilters, options?: RequestOptions): Promise; - search(filters?: TaskFilters, options?: RequestOptions): Promise; - get(id: string, options?: RequestOptions): Promise; - getPacket(id: string, options?: RequestOptions): Promise; - create(input: CreateTaskInput, options?: RequestOptions): Promise; - update( - id: string, - input: UpdateTaskInput, - options?: RequestOptions, - ): Promise; - archive( - id: string, - expectedRevision: number | string, - options?: RequestOptions, - ): Promise; - claim(id: string, options?: RequestOptions): Promise; - complete( - id: string, - result: string, - options?: RequestOptions, - ): Promise; - history( - id: string, - options?: HistoryRequestOptions, - ): Promise; - }; - memories: { - list( - filters?: MemoryFilters, - options?: RequestOptions, - ): Promise; - get(slug: string, options?: RequestOptions): Promise; - upsert( - slug: string, - input: UpsertMemoryInput, - options?: RequestOptions, - ): Promise; - delete?: ( - slug: string, - expectedRevision: number, - options?: RequestOptions, - ) => Promise; - history( - slug: string, - options?: HistoryRequestOptions, - ): Promise; - }; - prompts?: { - list( - filters?: PromptFilters, - options?: RequestOptions, - ): Promise; - get(id: string, options?: RequestOptions): Promise; - create(input: CreatePromptInput, options?: RequestOptions): Promise; - update( - id: string, - input: UpdatePromptInput, - options?: RequestOptions, - ): Promise; - delete( - id: string, - expectedRevision: number, - options?: RequestOptions, - ): Promise; - history( - id: string, - options?: HistoryRequestOptions, - ): Promise; - }; - search?: ( - request: SearchRequest, - options?: RequestOptions, - ) => Promise; - searchAdmin?: ( - input?: Readonly>, - options?: RequestOptions, - ) => Promise; + call( + name: Name, + input: OperationInput, + options?: WorkshopCallOptions, + ): Promise>; } export function createWorkshopClient( options: WorkshopClientOptions = {}, ): WorkshopClient { const transport = options.fetch ?? globalThis.fetch; - const base = (options.baseUrl ?? '').replace(/\/$/u, ''); - - const request = async ( - path: string, - init: WorkshopRequestInit = {}, - ): Promise => { - const token = await options.token?.(); - const { signal, ...requestInit } = init; - const response = await transport(`${base}${path}`, { - ...requestInit, - ...(signal ? { signal } : {}), - headers: { - accept: 'application/json', - ...(init.body ? { 'content-type': 'application/json' } : {}), - ...(token ? { authorization: `Bearer ${token}` } : {}), - ...init.headers, - }, - }); - const body = (await response.json().catch(() => null)) as - | { - error?: { - code?: string; - message?: string; - details?: Record; - }; - } - | T - | null; - if (!response.ok) { - const error = - body && typeof body === 'object' && 'error' in body - ? body.error - : undefined; - throw new WorkshopError( - (error?.code as ConstructorParameters[0]) ?? - 'internal_error', - error?.message ?? `Workshop request failed (${response.status})`, - response.status, - error?.details, - ); - } - return body as T; - }; - - const query = (filters: object | undefined) => { - const params = new URLSearchParams(); - for (const [key, value] of Object.entries(filters ?? {})) { - if (value !== undefined) params.set(key, String(value)); - } - const suffix = params.size ? `?${params}` : ''; - return suffix; - }; + const origin = normalizeOrigin(options.baseOrigin ?? ''); return { - tasks: { - list: (filters, requestOptions) => - request(`/api/workshop/tasks${query(filters)}`, { - signal: requestOptions?.signal, - }), - search: (filters, requestOptions) => - request(`/api/workshop/tasks${query(filters)}`, { - signal: requestOptions?.signal, - }), - get: (id, requestOptions) => - request(`/api/workshop/tasks/${encodeURIComponent(id)}`, { - signal: requestOptions?.signal, - }), - getPacket: (id, requestOptions) => - request(`/api/workshop/tasks/${encodeURIComponent(id)}/packet`, { - signal: requestOptions?.signal, - }), - create: (input, requestOptions) => - request('/api/workshop/tasks', { - method: 'POST', - body: JSON.stringify(input), - signal: requestOptions?.signal, - }), - update: (id, input, requestOptions) => - request(`/api/workshop/tasks/${encodeURIComponent(id)}`, { - method: 'PATCH', - body: JSON.stringify(input), - signal: requestOptions?.signal, - }), - archive: (id, expectedRevision, requestOptions) => - request(`/api/workshop/tasks/${encodeURIComponent(id)}`, { - method: 'DELETE', - headers: - typeof expectedRevision === 'number' - ? { 'x-workshop-revision': String(expectedRevision) } - : { 'if-match': expectedRevision }, - signal: requestOptions?.signal, - }), - claim: (id, requestOptions) => - request(`/api/workshop/tasks/${encodeURIComponent(id)}/claim`, { - method: 'POST', - signal: requestOptions?.signal, - }), - complete: (id, result, requestOptions) => - request(`/api/workshop/tasks/${encodeURIComponent(id)}/complete`, { - method: 'POST', - body: JSON.stringify({ result }), - signal: requestOptions?.signal, - }), - history: (id, requestOptions) => - request( - `/api/workshop/tasks/${encodeURIComponent(id)}/history${query({ limit: requestOptions?.limit })}`, - { - signal: requestOptions?.signal, - }, - ), - }, - memories: { - list: (filters, requestOptions) => - request(`/api/workshop/memories${query(filters)}`, { - signal: requestOptions?.signal, - }), - get: (slug, requestOptions) => - request(`/api/workshop/memories/${encodeURIComponent(slug)}`, { - signal: requestOptions?.signal, - }), - upsert: (slug, input, requestOptions) => - request(`/api/workshop/memories/${encodeURIComponent(slug)}`, { - method: 'PUT', - body: JSON.stringify(input), - signal: requestOptions?.signal, - }), - delete: (slug, expectedRevision, requestOptions) => - request(`/api/workshop/memories/${encodeURIComponent(slug)}`, { - method: 'DELETE', - headers: { 'x-workshop-revision': String(expectedRevision) }, - signal: requestOptions?.signal, - }), - history: (slug, requestOptions) => - request( - `/api/workshop/memories/${encodeURIComponent(slug)}/history${query({ limit: requestOptions?.limit })}`, - { - signal: requestOptions?.signal, - }, - ), - }, - prompts: { - list: (filters, requestOptions) => - request(`/api/workshop/prompts${query(filters)}`, { - signal: requestOptions?.signal, - }), - get: (id, requestOptions) => - request(`/api/workshop/prompts/${encodeURIComponent(id)}`, { - signal: requestOptions?.signal, - }), - create: (input, requestOptions) => - request('/api/workshop/prompts', { - method: 'POST', - body: JSON.stringify(input), - signal: requestOptions?.signal, - }), - update: (id, input, requestOptions) => - request(`/api/workshop/prompts/${encodeURIComponent(id)}`, { - method: 'PATCH', - body: JSON.stringify(input), - signal: requestOptions?.signal, - }), - delete: (id, expectedRevision, requestOptions) => - request(`/api/workshop/prompts/${encodeURIComponent(id)}`, { - method: 'DELETE', - headers: { 'x-workshop-revision': String(expectedRevision) }, - signal: requestOptions?.signal, - }), - history: (id, requestOptions) => - request( - `/api/workshop/prompts/${encodeURIComponent(id)}/history${query({ limit: requestOptions?.limit })}`, + async call( + name: Name, + input: OperationInput, + callOptions?: WorkshopCallOptions, + ): Promise> { + const token = await options.accessToken?.(); + if (token !== null && token !== undefined && !validToken(token)) + throw new WorkshopError( + 'validation_failed', + 'Access token is invalid', + 400, + ); + const init: RequestInit = { + method: 'POST', + body: JSON.stringify(input), + headers: { + accept: 'application/json', + 'content-type': 'application/json', + ...(token ? { authorization: `Bearer ${token}` } : {}), + }, + ...(callOptions?.signal ? { signal: callOptions.signal } : {}), + }; + const response = await transport( + `${origin}${OPERATION_ROUTE_PREFIX}/${name}`, + init, + ); + const body = (await response.json().catch(() => null)) as + | { + error?: { + code?: string; + message?: string; + requestId?: string; + details?: Record; + }; + } + | OperationOutput + | null; + if (!response.ok) { + const error = + body && typeof body === 'object' && 'error' in body + ? body.error + : undefined; + throw new WorkshopError( + (error?.code as ConstructorParameters[0]) ?? + 'internal_error', + error?.message ?? `Workshop request failed (${response.status})`, + response.status, { - signal: requestOptions?.signal, + ...(error?.requestId ? { requestId: error.requestId } : {}), + ...(error?.details ?? {}), }, - ), + ); + } + return body as OperationOutput; }, - search: (searchRequest, requestOptions) => - request('/api/workshop/search', { - method: 'POST', - body: JSON.stringify(searchRequest), - signal: requestOptions?.signal, - }), - searchAdmin: (input = {}, requestOptions) => - request('/api/workshop/search/admin', { - method: Object.keys(input).length ? 'POST' : 'GET', - ...(Object.keys(input).length ? { body: JSON.stringify(input) } : {}), - signal: requestOptions?.signal, - }), }; } + +function normalizeOrigin(value: string): string { + if (!value) return ''; + const url = new URL(value); + if ( + url.username || + url.password || + url.search || + url.hash || + (url.pathname !== '' && url.pathname !== '/') + ) + throw new WorkshopError( + 'validation_failed', + 'baseOrigin must be an origin without credentials, query, or path', + 400, + ); + return url.origin; +} + +function validToken(value: string): boolean { + return ( + value.length >= 1 && + value.length <= 16_384 && + !/[\u0000-\u0020\u007f]/u.test(value) + ); +} diff --git a/src/protocol/errors.ts b/src/protocol/errors.ts index d933e45..29c8b86 100644 --- a/src/protocol/errors.ts +++ b/src/protocol/errors.ts @@ -1,6 +1,7 @@ export type WorkshopErrorCode = | 'bad_request' | 'validation_failed' + | 'invalid_cursor' | 'unauthenticated' | 'forbidden' | 'not_found' @@ -48,6 +49,7 @@ function statusForCode(code: WorkshopErrorCode): number { switch (code) { case 'bad_request': case 'validation_failed': + case 'invalid_cursor': case 'query_rejected': return 400; case 'unauthenticated': diff --git a/src/resource-blobs.ts b/src/resource-blobs.ts new file mode 100644 index 0000000..83a4193 --- /dev/null +++ b/src/resource-blobs.ts @@ -0,0 +1,526 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { createReadStream, createWriteStream, type WriteStream } from 'node:fs'; +import { + lstat, + mkdir, + readFile, + realpath, + rename, + rm, + writeFile, +} from 'node:fs/promises'; +import { isAbsolute, join, resolve } from 'node:path'; +import { Readable } from 'node:stream'; +import type { + ResourceBlobBackupPort, + ResourceBlobDescriptor, + ResourceBlobRestorePort, +} from '@gnolith/taproot/backup'; +import type { PortableResourcePayloadStoreV1 } from '@gnolith/taproot'; +import { WorkshopError } from './protocol/errors.js'; + +const MANIFEST_FORMAT = 'workshop-resource-blob-store-v1'; +const MANIFEST_FILE = 'manifest.json'; +const MAX_MANIFEST_BYTES = 2 * 1024 * 1024; + +export interface LocalFilesystemResourceBlobOptionsV1 { + root: string; + maxBlobBytes?: number; + maxEntries?: number; +} + +export interface LocalFilesystemResourceBlobPortsV1 { + readonly backup: ResourceBlobBackupPort; + readonly restore: ResourceBlobRestorePort; + readonly payloadStore: PortableResourcePayloadStoreV1; +} + +interface Stage { + descriptor: ResourceBlobDescriptor; + path: string; + verified: boolean; +} + +/** + * Creates both Taproot blob ports over one Workshop-managed local store. + * Callers never derive, receive, or supply an on-disk object path. + */ +export async function createLocalFilesystemResourceBlobPortsV1( + options: LocalFilesystemResourceBlobOptionsV1, +): Promise { + const root = resolveRoot(options.root); + const maxBlobBytes = boundedInteger( + options.maxBlobBytes ?? 256 * 1024 * 1024, + 1, + 1024 * 1024 * 1024, + 'maxBlobBytes', + ); + const maxEntries = boundedInteger( + options.maxEntries ?? 100_000, + 1, + 1_000_000, + 'maxEntries', + ); + await secureDirectory(root); + const objects = join(root, 'objects'); + const staging = join(root, 'staging'); + await secureDirectory(objects); + await secureDirectory(staging); + const stages = new Map(); + + const readManifest = async (): Promise< + Map + > => { + const path = join(root, MANIFEST_FILE); + let bytes: Buffer; + try { + const stat = await lstat(path); + if (!stat.isFile() || stat.isSymbolicLink()) throw unsafeStore(); + if (stat.size > MAX_MANIFEST_BYTES) throw invalidManifest(); + bytes = await readFile(path); + } catch (error) { + if (isMissing(error)) return new Map(); + throw error; + } + let value: unknown; + try { + value = JSON.parse(bytes.toString('utf8')); + } catch { + throw invalidManifest(); + } + if (!isRecord(value) || value.format !== MANIFEST_FORMAT) + throw invalidManifest(); + if (!Array.isArray(value.items) || value.items.length > maxEntries) + throw invalidManifest(); + const entries = new Map(); + for (const item of value.items) { + const descriptor = normalizeDescriptor(item, maxBlobBytes); + if (entries.has(descriptor.referenceId)) throw invalidManifest(); + entries.set(descriptor.referenceId, descriptor); + } + return entries; + }; + + const writeManifest = async ( + entries: Map, + ): Promise => { + if (entries.size > maxEntries) + throw new WorkshopError( + 'validation_failed', + 'Resource blob manifest entry limit exceeded', + 400, + ); + const body = JSON.stringify({ + format: MANIFEST_FORMAT, + items: [...entries.values()].sort((a, b) => + compareCodeUnits(a.referenceId, b.referenceId), + ), + }); + if (Buffer.byteLength(body) > MAX_MANIFEST_BYTES) throw invalidManifest(); + const temporary = join(root, `.manifest-${randomUUID()}.tmp`); + await writeFile(temporary, body, { flag: 'wx', mode: 0o600 }); + await rename(temporary, join(root, MANIFEST_FILE)); + }; + + const backup: ResourceBlobBackupPort = { + async enumerate(cursor, limit) { + boundedInteger(limit, 1, 1000, 'limit'); + const after = decodeCursor(cursor); + const entries = [...(await readManifest()).values()].sort((a, b) => + compareCodeUnits(a.referenceId, b.referenceId), + ); + const page = entries + .filter(({ referenceId }) => compareCodeUnits(referenceId, after) > 0) + .slice(0, limit + 1); + const items = page.slice(0, limit); + return { + items, + cursor: + page.length > limit && items.length > 0 + ? encodeCursor(items.at(-1)!.referenceId) + : null, + }; + }, + async open(referenceId, signal) { + assertReferenceId(referenceId); + if (signal?.aborted) throw signal.reason; + const descriptor = (await readManifest()).get(referenceId); + if (!descriptor) + throw new WorkshopError('not_found', 'Resource blob not found', 404); + const path = objectPath(objects, descriptor.sha256); + const stat = await lstat(path).catch((error: unknown) => { + if (isMissing(error)) + throw new WorkshopError( + 'dependency_unavailable', + 'Resource blob payload is unavailable', + 503, + ); + throw error; + }); + if ( + !stat.isFile() || + stat.isSymbolicLink() || + stat.size !== descriptor.byteLength + ) + throw unsafeStore(); + return Readable.toWeb( + createReadStream(path, signal ? { signal } : undefined), + ) as ReadableStream; + }, + }; + + const restore: ResourceBlobRestorePort = { + async stage(rawDescriptor, stream) { + const descriptor = normalizeDescriptor(rawDescriptor, maxBlobBytes); + const stageId = randomUUID(); + const path = join(staging, `${stageId}.blob`); + const output = createWriteStream(path, { flags: 'wx', mode: 0o600 }); + try { + const digest = createHash('sha256'); + let byteLength = 0; + const reader = stream.getReader(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (!(value instanceof Uint8Array)) throw invalidBlob(); + byteLength += value.byteLength; + if (byteLength > descriptor.byteLength || byteLength > maxBlobBytes) + throw invalidBlob(); + digest.update(value); + await writeChunk(output, value); + } + } finally { + reader.releaseLock(); + } + await closeWriter(output); + if ( + byteLength !== descriptor.byteLength || + digest.digest('hex') !== descriptor.sha256 + ) + throw invalidBlob(); + stages.set(stageId, { descriptor, path, verified: false }); + return stageId; + } catch (error) { + output.destroy(); + await rm(path, { force: true }); + throw error; + } + }, + async verify(stageId) { + const stage = requireStage(stages, stageId); + const stat = await lstat(stage.path); + if ( + !stat.isFile() || + stat.isSymbolicLink() || + stat.size !== stage.descriptor.byteLength + ) + throw invalidBlob(); + const digest = createHash('sha256'); + for await (const chunk of createReadStream(stage.path)) + digest.update(chunk as Buffer); + if (digest.digest('hex') !== stage.descriptor.sha256) throw invalidBlob(); + stage.verified = true; + }, + async checkCollision(rawDescriptor) { + const descriptor = normalizeDescriptor(rawDescriptor, maxBlobBytes); + const existing = (await readManifest()).get(descriptor.referenceId); + if (!existing) return 'absent'; + return equalDescriptor(existing, descriptor) ? 'identical' : 'conflict'; + }, + rebind(stageId, canonicalReferenceId) { + assertReferenceId(canonicalReferenceId); + const stage = requireStage(stages, stageId); + stage.descriptor = { + ...stage.descriptor, + referenceId: canonicalReferenceId, + }; + return Promise.resolve(); + }, + async commit(stageIds) { + if (new Set(stageIds).size !== stageIds.length) + throw new WorkshopError( + 'validation_failed', + 'Duplicate blob stage', + 400, + ); + const selected = stageIds.map((id) => requireStage(stages, id)); + if (selected.some(({ verified }) => !verified)) + throw new WorkshopError( + 'validation_failed', + 'Resource blob stage is not verified', + 400, + ); + const entries = await readManifest(); + for (const stage of selected) { + const existing = entries.get(stage.descriptor.referenceId); + if (existing && !equalDescriptor(existing, stage.descriptor)) + throw new WorkshopError( + 'conflict', + 'Resource blob reference conflicts with existing content', + 409, + ); + } + for (const stage of selected) { + const destination = objectPath(objects, stage.descriptor.sha256); + try { + const stat = await lstat(destination); + if ( + !stat.isFile() || + stat.isSymbolicLink() || + stat.size !== stage.descriptor.byteLength + ) + throw unsafeStore(); + await rm(stage.path, { force: true }); + } catch (error) { + if (!isMissing(error)) throw error; + await rename(stage.path, destination); + } + entries.set(stage.descriptor.referenceId, stage.descriptor); + } + await writeManifest(entries); + for (const id of stageIds) stages.delete(id); + }, + async rollback(stageIds) { + await Promise.all( + stageIds.map(async (id) => { + const stage = stages.get(id); + if (!stage) return; + stages.delete(id); + await rm(stage.path, { force: true }); + }), + ); + }, + }; + + const payloadStore: PortableResourcePayloadStoreV1 = { + kind: 'taproot-resource-payload-store-v1', + async load(reference, signal) { + assertReferenceId(reference.referenceId); + boundedInteger(reference.byteLength, 0, maxBlobBytes, 'byteLength'); + signal?.throwIfAborted(); + const descriptor = (await readManifest()).get(reference.referenceId); + if (!descriptor) + throw new WorkshopError('not_found', 'Resource blob not found', 404); + if (descriptor.byteLength !== reference.byteLength) throw invalidBlob(); + const path = objectPath(objects, descriptor.sha256); + const stat = await lstat(path).catch((error: unknown) => { + if (isMissing(error)) + throw new WorkshopError( + 'dependency_unavailable', + 'Resource blob payload is unavailable', + 503, + ); + throw error; + }); + if ( + !stat.isFile() || + stat.isSymbolicLink() || + stat.size !== descriptor.byteLength + ) + throw unsafeStore(); + const bytes = await readFile(path, signal ? { signal } : undefined); + if ( + bytes.byteLength !== descriptor.byteLength || + createHash('sha256').update(bytes).digest('hex') !== descriptor.sha256 + ) + throw unsafeStore(); + return new Uint8Array(bytes); + }, + }; + + return { backup, restore, payloadStore }; +} + +function resolveRoot(value: string): string { + if (typeof value !== 'string' || !value || !isAbsolute(value)) + throw new WorkshopError( + 'validation_failed', + 'Resource blob root must be an absolute path', + 400, + ); + return resolve(value); +} + +async function secureDirectory(path: string): Promise { + await mkdir(path, { recursive: true, mode: 0o700 }); + const stat = await lstat(path); + const canonical = await realpath(path); + if ( + !stat.isDirectory() || + stat.isSymbolicLink() || + resolve(canonical).toLowerCase() !== resolve(path).toLowerCase() + ) + throw unsafeStore(); +} + +function objectPath(objects: string, sha256: string): string { + return join(objects, sha256); +} + +function normalizeDescriptor( + value: unknown, + maxBlobBytes: number, +): ResourceBlobDescriptor { + if (!isRecord(value)) throw invalidBlob(); + const descriptor = { + referenceId: value.referenceId, + resourceId: value.resourceId, + revision: value.revision, + byteLength: value.byteLength, + sha256: value.sha256, + mediaType: value.mediaType, + }; + assertReferenceId(descriptor.referenceId); + assertIdentifier(descriptor.resourceId, 'resourceId'); + boundedInteger(descriptor.revision, 1, Number.MAX_SAFE_INTEGER, 'revision'); + boundedInteger(descriptor.byteLength, 0, maxBlobBytes, 'byteLength'); + if ( + typeof descriptor.sha256 !== 'string' || + !/^[a-f0-9]{64}$/u.test(descriptor.sha256) || + typeof descriptor.mediaType !== 'string' || + descriptor.mediaType.length < 1 || + descriptor.mediaType.length > 255 || + /[\u0000-\u001f\u007f]/u.test(descriptor.mediaType) + ) + throw invalidBlob(); + return descriptor as ResourceBlobDescriptor; +} + +function assertReferenceId(value: unknown): asserts value is string { + assertIdentifier(value, 'referenceId'); +} + +function assertIdentifier( + value: unknown, + field: string, +): asserts value is string { + if ( + typeof value !== 'string' || + value.length < 1 || + value.length > 1024 || + /[\u0000-\u001f\u007f]/u.test(value) + ) + throw new WorkshopError( + 'validation_failed', + `Resource blob ${field} is invalid`, + 400, + ); +} + +function boundedInteger( + value: unknown, + minimum: number, + maximum: number, + field: string, +): number { + if ( + typeof value !== 'number' || + !Number.isSafeInteger(value) || + value < minimum || + value > maximum + ) + throw new WorkshopError( + 'validation_failed', + `Resource blob ${field} is invalid`, + 400, + ); + return value; +} + +function requireStage(stages: Map, stageId: string): Stage { + if (typeof stageId !== 'string') throw invalidBlob(); + const stage = stages.get(stageId); + if (!stage) + throw new WorkshopError('not_found', 'Resource blob stage not found', 404); + return stage; +} + +function equalDescriptor( + left: ResourceBlobDescriptor, + right: ResourceBlobDescriptor, +): boolean { + return ( + left.referenceId === right.referenceId && + left.resourceId === right.resourceId && + left.revision === right.revision && + left.byteLength === right.byteLength && + left.sha256 === right.sha256 && + left.mediaType === right.mediaType + ); +} + +function encodeCursor(referenceId: string): string { + return Buffer.from(referenceId, 'utf8').toString('base64url'); +} + +function decodeCursor(cursor: string | null): string { + if (cursor === null) return ''; + if ( + typeof cursor !== 'string' || + cursor.length < 1 || + cursor.length > 2048 || + !/^[A-Za-z0-9_-]+$/u.test(cursor) + ) + throw new WorkshopError('validation_failed', 'Blob cursor is invalid', 400); + const value = Buffer.from(cursor, 'base64url').toString('utf8'); + assertReferenceId(value); + if (encodeCursor(value) !== cursor) + throw new WorkshopError('validation_failed', 'Blob cursor is invalid', 400); + return value; +} + +function writeChunk(output: WriteStream, chunk: Uint8Array): Promise { + return new Promise((resolvePromise, reject) => { + output.write(chunk, (error) => (error ? reject(error) : resolvePromise())); + }); +} + +function closeWriter(output: WriteStream): Promise { + return new Promise((resolvePromise, reject) => { + output.end((error?: Error | null) => + error ? reject(error) : resolvePromise(), + ); + }); +} + +function compareCodeUnits(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function isMissing(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'code' in error && + error.code === 'ENOENT' + ); +} + +function invalidBlob(): WorkshopError { + return new WorkshopError( + 'validation_failed', + 'Resource blob payload or descriptor is invalid', + 400, + ); +} + +function invalidManifest(): WorkshopError { + return new WorkshopError( + 'dependency_unavailable', + 'Resource blob manifest is invalid', + 503, + ); +} + +function unsafeStore(): WorkshopError { + return new WorkshopError( + 'dependency_unavailable', + 'Resource blob store contains an unsafe filesystem object', + 503, + ); +} diff --git a/src/semantic-configuration.ts b/src/semantic-configuration.ts new file mode 100644 index 0000000..e5b9ece --- /dev/null +++ b/src/semantic-configuration.ts @@ -0,0 +1,483 @@ +import type { + SemanticConfigurationV1, + SemanticSearchAdminV1, +} from '@gnolith/taproot/search'; +import type { AuthorizationContext } from '@gnolith/taproot'; +import { WorkshopError } from './protocol/errors.js'; +import type { D1DatabaseLike } from './server/database.js'; +import { digestWorkshopOperationRequest } from './server/idempotency.js'; + +export interface ConfigureWorkshopSemanticsV1Input { + version: 1; + expectedRevision: number; + idempotencyKey: string; + configuration: SemanticConfigurationV1; +} + +export interface ConfigureWorkshopSemanticsV1Dependencies { + db: D1DatabaseLike; + installationId: string; + semantic: SemanticSearchAdminV1; + authorization: AuthorizationContext; + clock?: () => Date; +} + +export interface ConfigureWorkshopSemanticsV1Receipt { + version: 1; + configurationId: string; + revision: number; + fingerprint: string; + state: string; + ready: boolean; + replayed: boolean; +} + +export interface WorkshopSemanticStatusV1 { + state: 'ready' | 'degraded' | 'unconfigured'; + configured: boolean; + revision: number | null; + fingerprint: string | null; + ready: boolean; + backend: 'sqlite' | 'qdrant' | null; +} + +export async function configureWorkshopSemanticsV1( + raw: ConfigureWorkshopSemanticsV1Input, + dependencies: ConfigureWorkshopSemanticsV1Dependencies, +): Promise { + const input = exactInput(raw); + if ( + dependencies.authorization.installationId !== dependencies.installationId || + !dependencies.authorization.capabilities.includes('search:admin') + ) + throw forbidden(); + const requestDigest = await digestWorkshopOperationRequest({ + operation: 'semantic.configure', + expectedRevision: input.expectedRevision, + configuration: input.configuration, + }); + const prior = await receipt( + dependencies.db, + dependencies.installationId, + dependencies.authorization.principalId, + input.idempotencyKey, + ); + if (prior) { + if (prior.request_digest !== requestDigest) throw conflict(); + return { ...parseReceipt(prior.response_json), replayed: true }; + } + try { + await dependencies.semantic.configure( + input.configuration, + { + expectedRevision: input.expectedRevision, + idempotencyKey: input.idempotencyKey, + }, + dependencies.authorization, + ); + await dependencies.semantic.select( + input.configuration.id, + dependencies.authorization, + ); + } catch (error) { + mapSemanticError(error); + } + const status = await dependencies.semantic.status(dependencies.authorization); + if ( + status.revision === null || + status.fingerprint === null || + !/^[a-f0-9]{64}$/u.test(status.fingerprint) + ) + throw new WorkshopError( + 'dependency_unavailable', + 'Semantic configuration status is incomplete', + 503, + ); + const response: ConfigureWorkshopSemanticsV1Receipt = { + version: 1, + configurationId: input.configuration.id, + revision: status.revision, + fingerprint: status.fingerprint, + state: status.state, + ready: status.ready, + replayed: false, + }; + const now = (dependencies.clock ?? (() => new Date()))().toISOString(); + try { + await dependencies.db.batch([ + dependencies.db + .prepare( + `INSERT INTO workshop_semantic_configuration_state ( + installation_id, revision, fingerprint, configuration_json, + ready, updated_at + ) VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(installation_id) DO UPDATE SET + revision = excluded.revision, + fingerprint = excluded.fingerprint, + configuration_json = excluded.configuration_json, + ready = excluded.ready, + updated_at = excluded.updated_at`, + ) + .bind( + dependencies.installationId, + response.revision, + response.fingerprint, + JSON.stringify(input.configuration), + response.ready ? 1 : 0, + now, + ), + dependencies.db + .prepare( + `INSERT INTO workshop_semantic_configuration_receipts ( + installation_id, actor_principal_id, idempotency_key, + request_digest, response_json, created_at + ) VALUES (?, ?, ?, ?, ?, ?)`, + ) + .bind( + dependencies.installationId, + dependencies.authorization.principalId, + input.idempotencyKey, + requestDigest, + JSON.stringify(response), + now, + ), + ]); + } catch (error) { + const concurrent = await receipt( + dependencies.db, + dependencies.installationId, + dependencies.authorization.principalId, + input.idempotencyKey, + ); + if (!concurrent || concurrent.request_digest !== requestDigest) throw error; + return { ...parseReceipt(concurrent.response_json), replayed: true }; + } + return response; +} + +export async function readWorkshopSemanticStatusV1( + db: D1DatabaseLike, + installationId: string, +): Promise { + const row = await db + .prepare( + `SELECT revision, fingerprint, ready, configuration_json + FROM workshop_semantic_configuration_state + WHERE installation_id = ?`, + ) + .bind(installationId) + .first<{ + revision: number; + fingerprint: string; + ready: number; + configuration_json: string; + }>(); + if (!row) + return { + state: 'unconfigured', + configured: false, + revision: null, + fingerprint: null, + ready: false, + backend: null, + }; + return { + state: row.ready === 1 ? 'ready' : 'degraded', + configured: true, + revision: Number(row.revision), + fingerprint: row.fingerprint, + ready: row.ready === 1, + backend: semanticBackend(row.configuration_json), + }; +} + +function semanticBackend(value: string): 'sqlite' | 'qdrant' { + try { + const parsed = JSON.parse(value) as { vector?: { kind?: unknown } }; + if (parsed.vector?.kind === 'sqlite' || parsed.vector?.kind === 'qdrant') + return parsed.vector.kind; + } catch { + // Fail closed below without returning persisted configuration content. + } + throw new WorkshopError( + 'internal_error', + 'Semantic configuration status is invalid', + 500, + ); +} + +function exactInput( + value: ConfigureWorkshopSemanticsV1Input, +): ConfigureWorkshopSemanticsV1Input { + const input = object(value); + exactKeys(input, [ + 'configuration', + 'expectedRevision', + 'idempotencyKey', + 'version', + ]); + if (input.version !== 1) + throw invalid('Semantic configuration version is invalid'); + const expectedRevision = integer(input.expectedRevision, true); + const idempotencyKey = identifier(input.idempotencyKey); + const configuration = semanticConfiguration(input.configuration); + return { version: 1, expectedRevision, idempotencyKey, configuration }; +} + +function semanticConfiguration(value: unknown): SemanticConfigurationV1 { + const configuration = object(value); + exactKeys(configuration, ['id', 'name', 'provider', 'vector', 'version']); + if (configuration.version !== 1) + throw invalid('Semantic configuration version is invalid'); + const provider = object(configuration.provider); + exactKeys(provider, [ + 'allowPrivateEndpoint', + 'credentialSelector', + 'dimensions', + 'endpoint', + 'kind', + 'metric', + 'model', + 'redirectPolicy', + ]); + if ( + provider.kind !== 'openai-compatible' && + provider.kind !== 'ollama-compatible' + ) + throw invalid('Semantic provider profile is invalid'); + const allowPrivateEndpoint = boolean(provider.allowPrivateEndpoint); + const providerEndpoint = fullSemanticProviderEndpoint( + provider.kind, + endpoint(provider.endpoint, allowPrivateEndpoint), + ); + const model = identifier(provider.model, 512); + const dimensions = integer(provider.dimensions); + if (dimensions > 65_536) + throw invalid('Semantic embedding dimensions are invalid'); + if (!['cosine', 'dot', 'euclid'].includes(String(provider.metric))) + throw invalid('Semantic vector metric is invalid'); + if (provider.redirectPolicy !== 'error') + throw invalid('Semantic redirect policy must fail closed'); + const credentialSelector = selector(provider.credentialSelector); + const vector = object(configuration.vector); + if (vector.kind === 'sqlite') { + exactKeys(vector, ['kind']); + return { + version: 1, + id: identifier(configuration.id), + name: text(configuration.name, 1024), + provider: { + kind: provider.kind, + endpoint: providerEndpoint, + model, + dimensions, + metric: provider.metric as 'cosine' | 'dot' | 'euclid', + credentialSelector, + allowPrivateEndpoint, + redirectPolicy: 'error', + }, + vector: { kind: 'sqlite' }, + }; + } + exactKeys(vector, [ + 'allowPrivateEndpoint', + 'collection', + 'credentialSelector', + 'endpoint', + 'kind', + 'redirectPolicy', + ]); + if (vector.kind !== 'qdrant' || vector.redirectPolicy !== 'error') + throw invalid('Semantic vector profile is invalid'); + const vectorPrivate = boolean(vector.allowPrivateEndpoint); + return { + version: 1, + id: identifier(configuration.id), + name: text(configuration.name, 1024), + provider: { + kind: provider.kind, + endpoint: providerEndpoint, + model, + dimensions, + metric: provider.metric as 'cosine' | 'dot' | 'euclid', + credentialSelector, + allowPrivateEndpoint, + redirectPolicy: 'error', + }, + vector: { + kind: 'qdrant', + endpoint: endpoint(vector.endpoint, vectorPrivate), + collection: identifier(vector.collection, 256), + credentialSelector: selector(vector.credentialSelector), + allowPrivateEndpoint: vectorPrivate, + redirectPolicy: 'error', + }, + }; +} + +async function receipt( + db: D1DatabaseLike, + installationId: string, + principalId: string, + idempotencyKey: string, +) { + return db + .prepare( + `SELECT request_digest, response_json + FROM workshop_semantic_configuration_receipts + WHERE installation_id = ? AND actor_principal_id = ? + AND idempotency_key = ?`, + ) + .bind(installationId, principalId, idempotencyKey) + .first<{ request_digest: string; response_json: string }>(); +} + +function parseReceipt(value: string): ConfigureWorkshopSemanticsV1Receipt { + try { + return JSON.parse(value) as ConfigureWorkshopSemanticsV1Receipt; + } catch { + throw new WorkshopError( + 'internal_error', + 'Semantic configuration receipt is invalid', + 500, + ); + } +} + +function mapSemanticError(error: unknown): never { + const candidate = error as { name?: unknown; code?: unknown }; + if ( + candidate?.name === 'AuthorizationDeniedError' || + candidate?.code === 'AUTHORIZATION_DENIED' + ) + throw forbidden(); + if ( + candidate?.name === 'RevisionConflictError' || + candidate?.code === 'REVISION_CONFLICT' + ) + throw conflict(); + if ( + candidate?.name === 'TypeError' || + candidate?.code === 'INVALID_CONFIGURATION' + ) + throw invalid('Semantic configuration is invalid'); + if (error instanceof WorkshopError) throw error; + throw new WorkshopError( + 'dependency_unavailable', + 'Semantic configuration failed', + 503, + ); +} + +function endpoint(value: unknown, allowPrivate: boolean): string { + const raw = text(value, 4096); + let parsed: URL; + try { + parsed = new URL(raw); + } catch { + throw invalid('Semantic endpoint is invalid'); + } + if ( + parsed.username || + parsed.password || + !['https:', ...(allowPrivate ? ['http:'] : [])].includes(parsed.protocol) + ) + throw invalid('Semantic endpoint is invalid'); + if ( + !allowPrivate && + (parsed.hostname === 'localhost' || + parsed.hostname.endsWith('.localhost') || + /^(?:127\.|10\.|192\.168\.|169\.254\.)/u.test(parsed.hostname) || + parsed.hostname === '::1') + ) + throw invalid('Private semantic endpoint requires explicit approval'); + return parsed.href; +} + +function fullSemanticProviderEndpoint( + kind: 'openai-compatible' | 'ollama-compatible', + endpointValue: string, +): string { + const parsed = new URL(endpointValue); + const terminal = kind === 'openai-compatible' ? '/embeddings' : '/api/embed'; + if ( + parsed.pathname.replace(/\/+$/u, '') !== terminal && + !parsed.pathname.replace(/\/+$/u, '').endsWith(terminal) + ) + throw invalid( + `Semantic ${kind} endpoint must be the exact ${terminal} POST URL`, + ); + if (parsed.search || parsed.hash) + throw invalid( + 'Semantic provider endpoint cannot contain query or fragment', + ); + parsed.pathname = parsed.pathname.replace(/\/+$/u, ''); + return parsed.href; +} + +function selector(value: unknown): string | null { + return value === null ? null : identifier(value, 256); +} + +function object(value: unknown): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) + throw invalid('Expected an object'); + return value as Record; +} + +function exactKeys( + value: Record, + expected: readonly string[], +): void { + if ( + JSON.stringify(Object.keys(value).sort()) !== + JSON.stringify([...expected].sort()) + ) + throw invalid('Semantic configuration has unknown or missing fields'); +} + +function text(value: unknown, limit: number): string { + if ( + typeof value !== 'string' || + !value.trim() || + value.length > limit || + value.includes('\u0000') + ) + throw invalid('Expected bounded text'); + return value; +} + +function identifier(value: unknown, limit = 256): string { + const result = text(value, limit); + if (!/^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/u.test(result)) + throw invalid('Semantic selector or identifier is invalid'); + return result; +} + +function integer(value: unknown, zero = false): number { + const minimum = zero ? 0 : 1; + if (!Number.isSafeInteger(value) || Number(value) < minimum) + throw invalid('Expected a bounded integer'); + return Number(value); +} + +function boolean(value: unknown): boolean { + if (typeof value !== 'boolean') throw invalid('Expected a boolean'); + return value; +} + +function invalid(message: string): WorkshopError { + return new WorkshopError('validation_failed', message, 400); +} + +function forbidden(): WorkshopError { + return new WorkshopError('forbidden', 'Authorization denied', 403); +} + +function conflict(): WorkshopError { + return new WorkshopError( + 'conflict', + 'Semantic configuration revision or idempotency key conflicts', + 409, + ); +} diff --git a/src/server.ts b/src/server.ts index 56e1862..f9279c5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,15 +1,16 @@ -export * from './server/context.js'; -export type { WorkshopCursorCodec } from './server/cursor.js'; export * from './server/database.js'; -export * from './server/limits.js'; -export * from './server/validation.js'; export * from './server/authorization.js'; export * from './server/authorization-readiness.js'; -export * from './server/observability.js'; +export * from './server/health.js'; export * from './server/memories.js'; export * from './server/prompts.js'; -export * from './server/search.js'; export * from './server/tasks.js'; -export * from './server/packets.js'; -export * from './server/knowledge.js'; -export * from './server/health.js'; +export * from './server/canonical-producer.js'; +export * from './server/auth.js'; +export * from './server/assets.js'; +export * from './server/config.js'; +export * from './server/lifecycle.js'; +export * from './server/service.js'; +export * from './server/search.js'; +export * from './backup.js'; +export * from './producers.js'; diff --git a/src/server/assets.ts b/src/server/assets.ts new file mode 100644 index 0000000..a149de5 --- /dev/null +++ b/src/server/assets.ts @@ -0,0 +1,249 @@ +import { WorkshopError } from '../protocol/errors.js'; + +const MEDIA_TYPES = new Set([ + 'application/javascript', + 'application/json', + 'application/manifest+json', + 'application/wasm', + 'font/woff2', + 'image/avif', + 'image/png', + 'image/svg+xml', + 'image/webp', + 'text/css', + 'text/html; charset=utf-8', +]); +const RESERVED_PREFIXES = ['/mcp', '/health', '/api', '/.well-known']; +export const WORKSHOP_RECOMMENDED_ASSET_PREFIX = '/app' as const; + +export interface WaystoneAssetManifest { + format: 'waystone-assets-v1'; + files: readonly { + path: string; + sha256: string; + bytes: number; + mediaType: string; + immutable: true; + }[]; + entrypoint: string; + manifestSha256: string; +} + +export interface WaystoneAssetSource { + read(path: string): Promise<{ bytes: Uint8Array; symlink: boolean } | null>; +} + +export interface VerifiedWaystoneAssets { + readonly prefix: string; + readonly manifest: WaystoneAssetManifest; + serve(pathname: string): Promise; +} + +export async function verifyWaystoneAssetMount(input: { + prefix: string; + manifest: unknown; + source: WaystoneAssetSource; +}): Promise { + const prefix = validateWorkshopAssetPrefix(input.prefix); + const manifest = exactManifest(input.manifest); + const withoutDigest = { + format: manifest.format, + files: manifest.files, + entrypoint: manifest.entrypoint, + }; + const manifestDigest = await sha256Hex( + new TextEncoder().encode(canonicalize(withoutDigest)), + ); + if (!constantTimeHex(manifestDigest, manifest.manifestSha256.toLowerCase())) + throw invalidAssets('Waystone manifest digest mismatch'); + const paths = manifest.files.map(({ path }) => path); + if ( + paths.some((path) => path !== normalizeAssetPath(path)) || + paths.some((path, index) => index > 0 && paths[index - 1]! >= path) + ) + throw invalidAssets( + 'Waystone asset paths must be normalized, unique, and sorted', + ); + if (!paths.includes(manifest.entrypoint)) + throw invalidAssets('Waystone entrypoint is not in the manifest'); + + const bytesByPath = new Map(); + for (const file of manifest.files) { + if ( + file.immutable !== true || + !MEDIA_TYPES.has(file.mediaType) || + !Number.isSafeInteger(file.bytes) || + file.bytes < 0 || + !/^[a-f0-9]{64}$/u.test(file.sha256) + ) + throw invalidAssets('Waystone file metadata is invalid'); + const loaded = await input.source.read(file.path); + if (!loaded || loaded.symlink) + throw invalidAssets('Waystone asset is missing or a symlink'); + if ( + loaded.bytes.byteLength !== file.bytes || + !constantTimeHex(await sha256Hex(loaded.bytes), file.sha256) + ) + throw invalidAssets('Waystone asset bytes do not match the manifest'); + bytesByPath.set(file.path, loaded.bytes.slice()); + } + + return { + prefix, + manifest, + serve(pathname) { + if (!pathname.startsWith(`${prefix}/`) && pathname !== prefix) + return Promise.resolve(null); + let normalized: string; + try { + const suffix = + pathname === prefix || pathname === `${prefix}/` + ? manifest.entrypoint + : decodeURIComponent(pathname.slice(prefix.length + 1)); + normalized = normalizeAssetPath(suffix); + } catch { + return Promise.resolve(null); + } + const file = manifest.files.find(({ path }) => path === normalized); + const bytes = bytesByPath.get(normalized); + if (!file || !bytes) return Promise.resolve(null); + return Promise.resolve( + new Response(bytes.slice(), { + headers: { + 'content-type': file.mediaType, + 'content-length': String(file.bytes), + 'cache-control': 'public, max-age=31536000, immutable', + 'x-content-type-options': 'nosniff', + }, + }), + ); + }, + }; +} + +function exactManifest(value: unknown): WaystoneAssetManifest { + if ( + !recordWithKeys(value, ['entrypoint', 'files', 'format', 'manifestSha256']) + ) + throw invalidAssets('Waystone manifest shape is invalid'); + if ( + value.format !== 'waystone-assets-v1' || + !Array.isArray(value.files) || + typeof value.entrypoint !== 'string' || + !/^[a-f0-9]{64}$/u.test(String(value.manifestSha256)) + ) + throw invalidAssets('Waystone manifest metadata is invalid'); + const files = value.files.map((file) => { + if ( + !recordWithKeys(file, [ + 'bytes', + 'immutable', + 'mediaType', + 'path', + 'sha256', + ]) || + typeof file.path !== 'string' || + typeof file.sha256 !== 'string' || + typeof file.bytes !== 'number' || + typeof file.mediaType !== 'string' || + file.immutable !== true + ) + throw invalidAssets('Waystone file entry is invalid'); + return { + path: file.path, + sha256: file.sha256, + bytes: file.bytes, + mediaType: file.mediaType, + immutable: true as const, + }; + }); + return { + format: 'waystone-assets-v1', + files, + entrypoint: value.entrypoint, + manifestSha256: String(value.manifestSha256), + }; +} + +export function validateWorkshopAssetPrefix(value: string): string { + if ( + typeof value !== 'string' || + !/^\/[a-z0-9][a-z0-9_-]*(?:\/[a-z0-9][a-z0-9_-]*)*$/u.test(value) || + RESERVED_PREFIXES.some( + (reserved) => + value === reserved || + value.startsWith(`${reserved}/`) || + reserved.startsWith(`${value}/`), + ) + ) + throw invalidAssets('Static asset prefix is invalid or reserved'); + return value; +} + +function normalizeAssetPath(value: string): string { + if ( + typeof value !== 'string' || + value.length < 1 || + value.length > 1024 || + value.startsWith('/') || + value.includes('\\') || + value.includes('\u0000') + ) + throw invalidAssets('Waystone asset path is invalid'); + const segments = value.split('/'); + if ( + segments.some( + (segment) => + !segment || + segment === '.' || + segment === '..' || + !/^[A-Za-z0-9._-]+$/u.test(segment), + ) + ) + throw invalidAssets('Waystone asset path is invalid'); + return segments.join('/'); +} + +function recordWithKeys( + value: unknown, + expected: readonly string[], +): value is Record { + return ( + Boolean(value) && + typeof value === 'object' && + !Array.isArray(value) && + JSON.stringify(Object.keys(value as object).sort()) === + JSON.stringify([...expected].sort()) + ); +} + +function canonicalize(value: unknown): string { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) + return `[${value.map((entry) => canonicalize(entry)).join(',')}]`; + const object = value as Record; + return `{${Object.keys(object) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalize(object[key])}`) + .join(',')}}`; +} + +async function sha256Hex(bytes: Uint8Array): Promise { + const digest = await crypto.subtle.digest('SHA-256', Uint8Array.from(bytes)); + return [...new Uint8Array(digest)] + .map((byte) => byte.toString(16).padStart(2, '0')) + .join(''); +} + +function constantTimeHex(left: string, right: string): boolean { + if (!/^[a-f0-9]{64}$/u.test(left) || !/^[a-f0-9]{64}$/u.test(right)) + return false; + let difference = 0; + for (let index = 0; index < 64; index += 1) + difference |= left.charCodeAt(index) ^ right.charCodeAt(index); + return difference === 0; +} + +function invalidAssets(message: string): WorkshopError { + return new WorkshopError('validation_failed', message, 400); +} diff --git a/src/server/auth.ts b/src/server/auth.ts new file mode 100644 index 0000000..1eeda3f --- /dev/null +++ b/src/server/auth.ts @@ -0,0 +1,479 @@ +import { WorkshopError } from '../protocol/errors.js'; +import type { AuthenticatedPrincipal } from '../protocol.js'; +import type { D1DatabaseLike } from './database.js'; + +const LOCAL_PREFIX = new TextEncoder().encode('gnolith-local-bearer-v1\u0000'); +const BASE64URL_32 = /^[A-Za-z0-9_-]{43}$/u; +const MAX_AUTHORIZATION_HEADER = 16_512; +const MAX_OAUTH_TOKEN = 16_384; + +export interface LocalBearerCredentialRecord { + profile: 'local-bearer-v1'; + credentialId: string; + tokenDigest: string; + installationId: string; + principalId: string; + activeWorkspaceId: string | null; + capabilities: readonly string[]; + authorizationRevision: number; + disabled: boolean; +} + +export interface LocalBearerCredentialStore { + findByDigest( + digest: string, + signal: AbortSignal, + ): Promise; +} + +export interface LocalBearerAuthorizationResolverV1 { + resolve(input: { + installationId: string; + principalId: string; + workspaceId: string; + signal: AbortSignal; + }): Promise<{ + capabilities: readonly string[]; + authorizationRevision: number; + }>; +} + +/** Reads only Workshop's schema-v8 credential table. */ +export function createWorkshopLocalBearerCredentialStoreV1( + db: D1DatabaseLike, + authorization: LocalBearerAuthorizationResolverV1, +): LocalBearerCredentialStore { + return { + async findByDigest(digest, signal) { + if (signal.aborted) throw signal.reason; + if (!/^[a-f0-9]{64}$/u.test(digest)) return null; + const row = await db + .prepare( + `SELECT installation_id, credential_id, token_digest, principal_id, + workspace_id, disabled + FROM workshop_credentials + WHERE token_digest = ?`, + ) + .bind(digest) + .first<{ + installation_id: string; + credential_id: string; + token_digest: string; + principal_id: string; + workspace_id: string; + disabled: number; + }>(); + if (!row) return null; + const resolved = await authorization.resolve({ + installationId: row.installation_id, + principalId: row.principal_id, + workspaceId: row.workspace_id, + signal, + }); + return { + profile: 'local-bearer-v1', + credentialId: row.credential_id, + tokenDigest: row.token_digest, + installationId: row.installation_id, + principalId: row.principal_id, + activeWorkspaceId: row.workspace_id, + capabilities: [...resolved.capabilities], + authorizationRevision: resolved.authorizationRevision, + disabled: row.disabled !== 0, + }; + }, + }; +} + +export interface AuthorizationFence { + recheck( + principal: AuthenticatedPrincipal, + signal: AbortSignal, + ): Promise; +} + +export interface LocalBearerAuthOptions { + profile: 'local-bearer-v1'; + credentials: LocalBearerCredentialStore; + authorization: AuthorizationFence; +} + +export interface RemoteOAuthConfig { + profile: 'remote-oauth-v1'; + issuer: string; + audience: string; + algorithms: readonly string[]; + requiredScopes: readonly string[]; + resource: string; + authorizationServers: readonly string[]; + scopeCapabilityMap: Readonly>; + principalClaim: string; + workspaceClaim: string; + installationClaim: string; + credentialClaim: string; + maxClockSkewSeconds: number; +} + +export interface VerifiedJwt { + header: Readonly>; + claims: Readonly>; +} + +export interface RemoteJwtVerifier { + verify( + token: string, + config: RemoteOAuthConfig, + signal: AbortSignal, + ): Promise; +} + +export interface RemotePrincipalResolver { + resolve( + identity: { + installationId: string; + principalId: string; + activeWorkspaceId: string | null; + credentialId: string; + scopes: readonly string[]; + capabilities: readonly string[]; + }, + signal: AbortSignal, + ): Promise; +} + +export interface RemoteOAuthAuthOptions { + profile: 'remote-oauth-v1'; + config: RemoteOAuthConfig; + jwt: RemoteJwtVerifier; + principals: RemotePrincipalResolver; + authorization: AuthorizationFence; + clock?: () => Date; +} + +export type AuthenticationOptions = + LocalBearerAuthOptions | RemoteOAuthAuthOptions; + +export interface AuthenticationVerifier { + readonly profile: AuthenticationOptions['profile']; + readonly challenge: string; + authenticate(request: Request): Promise; + protectedResourceMetadata(): Record | null; +} + +export function createAuthenticationVerifier( + options: AuthenticationOptions, +): AuthenticationVerifier { + return options.profile === 'local-bearer-v1' + ? localVerifier(options) + : remoteVerifier(options); +} + +export async function digestLocalBearerToken( + tokenBytes: Uint8Array, +): Promise { + if (tokenBytes.byteLength !== 32) throw invalidCredential(); + const material = new Uint8Array(LOCAL_PREFIX.length + tokenBytes.length); + material.set(LOCAL_PREFIX); + material.set(tokenBytes, LOCAL_PREFIX.length); + try { + return hex(await crypto.subtle.digest('SHA-256', material)); + } finally { + material.fill(0); + } +} + +/** + * Computes the exact digest accepted by authorization_admin credential-register. + * The bearer text is never persisted or returned. + */ +export async function digestLocalBearerCredentialText( + token: string, +): Promise { + if (!BASE64URL_32.test(token)) throw invalidCredential(); + const decoded = base64urlDecode(token); + if (decoded.byteLength !== 32 || base64urlEncode(decoded) !== token) { + decoded.fill(0); + throw invalidCredential(); + } + try { + return await digestLocalBearerToken(decoded); + } finally { + decoded.fill(0); + } +} + +export function decodeLocalBearerCredentialFile(bytes: Uint8Array): Uint8Array { + if (bytes.byteLength !== 43 && bytes.byteLength !== 44) + throw invalidCredential(); + if (bytes.byteLength === 44 && bytes[43] !== 0x0a) throw invalidCredential(); + const encoded = new TextDecoder('utf-8', { fatal: true }).decode( + bytes.subarray(0, 43), + ); + if (!BASE64URL_32.test(encoded)) throw invalidCredential(); + const decoded = base64urlDecode(encoded); + if (decoded.byteLength !== 32 || base64urlEncode(decoded) !== encoded) { + decoded.fill(0); + throw invalidCredential(); + } + return decoded; +} + +function localVerifier( + options: LocalBearerAuthOptions, +): AuthenticationVerifier { + return { + profile: 'local-bearer-v1', + challenge: 'Bearer', + protectedResourceMetadata: () => null, + async authenticate(request) { + rejectAlternateTokenSources(request); + const token = bearerToken(request); + if (!BASE64URL_32.test(token)) throw unauthenticated(); + const decoded = base64urlDecode(token); + if (decoded.byteLength !== 32) throw unauthenticated(); + let digest: string; + try { + digest = await digestLocalBearerToken(decoded); + } finally { + decoded.fill(0); + } + const record = await options.credentials.findByDigest( + digest, + request.signal, + ); + if (!record || record.disabled) throw unauthenticated(); + if (!constantTimeHex(record.tokenDigest, digest)) throw unauthenticated(); + return options.authorization.recheck( + { + installationId: record.installationId, + principalId: record.principalId, + activeWorkspaceId: record.activeWorkspaceId, + capabilities: [...record.capabilities], + authorizationRevision: record.authorizationRevision, + credentialId: record.credentialId, + }, + request.signal, + ); + }, + }; +} + +function remoteVerifier( + options: RemoteOAuthAuthOptions, +): AuthenticationVerifier { + validateRemoteConfig(options.config); + const metadataUrl = + `${new URL(options.config.resource).origin}/` + + '.well-known/oauth-protected-resource/mcp'; + return { + profile: 'remote-oauth-v1', + challenge: `Bearer resource_metadata="${metadataUrl}"`, + protectedResourceMetadata: () => ({ + resource: options.config.resource, + authorization_servers: [...options.config.authorizationServers], + bearer_methods_supported: ['header'], + scopes_supported: [...options.config.requiredScopes], + }), + async authenticate(request) { + rejectAlternateTokenSources(request); + const token = bearerToken(request); + if (token.length > MAX_OAUTH_TOKEN) throw unauthenticated(); + const verified = await options.jwt.verify( + token, + options.config, + request.signal, + ); + const now = (options.clock ?? (() => new Date()))().getTime() / 1000; + const skew = options.config.maxClockSkewSeconds; + const algorithm = requiredClaim(verified.header, 'alg'); + if (!options.config.algorithms.includes(algorithm)) + throw unauthenticated(); + if (requiredClaim(verified.claims, 'iss') !== options.config.issuer) + throw unauthenticated(); + const audience = verified.claims.aud; + const audiences = Array.isArray(audience) + ? audience.filter((value): value is string => typeof value === 'string') + : typeof audience === 'string' + ? [audience] + : []; + if (!audiences.includes(options.config.audience)) throw unauthenticated(); + const expires = numericClaim(verified.claims, 'exp'); + const notBefore = + verified.claims.nbf === undefined + ? 0 + : numericClaim(verified.claims, 'nbf'); + if (expires < now - skew || notBefore > now + skew) + throw unauthenticated(); + const scopes = scopeSet(verified.claims.scope); + if ( + options.config.requiredScopes.some( + (required) => !scopes.includes(required), + ) + ) + throw new WorkshopError('forbidden', 'Authorization denied', 403); + const capabilities = [ + ...new Set( + scopes + .map((scope) => options.config.scopeCapabilityMap[scope]) + .filter((value): value is string => Boolean(value)), + ), + ].sort(); + const resolved = await options.principals.resolve( + { + installationId: requiredClaim( + verified.claims, + options.config.installationClaim, + ), + principalId: requiredClaim( + verified.claims, + options.config.principalClaim, + ), + activeWorkspaceId: optionalClaim( + verified.claims, + options.config.workspaceClaim, + ), + credentialId: requiredClaim( + verified.claims, + options.config.credentialClaim, + ), + scopes, + capabilities, + }, + request.signal, + ); + return options.authorization.recheck(resolved, request.signal); + }, + }; +} + +function validateRemoteConfig(config: RemoteOAuthConfig): void { + if ( + new URL(config.issuer).protocol !== 'https:' || + new URL(config.resource).protocol !== 'https:' || + config.authorizationServers.length === 0 || + config.authorizationServers.some( + (server) => new URL(server).protocol !== 'https:', + ) || + config.algorithms.length === 0 || + config.algorithms.some((algorithm) => algorithm === 'none') || + config.requiredScopes.length === 0 || + !Number.isSafeInteger(config.maxClockSkewSeconds) || + config.maxClockSkewSeconds < 0 || + config.maxClockSkewSeconds > 300 + ) + throw new WorkshopError( + 'validation_failed', + 'Remote OAuth configuration is invalid', + 400, + ); +} + +function rejectAlternateTokenSources(request: Request): void { + const url = new URL(request.url); + for (const key of url.searchParams.keys()) { + if (/token|authorization|credential/iu.test(key)) throw unauthenticated(); + } + if ( + request.headers.has('cookie') || + request.headers.has('x-api-key') || + request.headers.has('proxy-authorization') + ) + throw unauthenticated(); +} + +function bearerToken(request: Request): string { + const header = request.headers.get('authorization'); + if ( + !header || + header.length > MAX_AUTHORIZATION_HEADER || + !header.startsWith('Bearer ') || + header.slice(7).length === 0 || + /[\u0000-\u0020\u007f]/u.test(header.slice(7)) + ) + throw unauthenticated(); + return header.slice(7); +} + +function requiredClaim( + claims: Readonly>, + name: string, +): string { + const value = claims[name]; + if (typeof value !== 'string' || !value || value.length > 4096) + throw unauthenticated(); + return value; +} + +function optionalClaim( + claims: Readonly>, + name: string, +): string | null { + const value = claims[name]; + if (value === null || value === undefined) return null; + if (typeof value !== 'string' || !value || value.length > 4096) + throw unauthenticated(); + return value; +} + +function numericClaim( + claims: Readonly>, + name: string, +): number { + const value = claims[name]; + if (typeof value !== 'number' || !Number.isFinite(value)) + throw unauthenticated(); + return value; +} + +function scopeSet(value: unknown): string[] { + if (typeof value !== 'string' || value.length > 16_384) + throw unauthenticated(); + return [...new Set(value.split(' ').filter(Boolean))].sort(); +} + +function base64urlDecode(value: string): Uint8Array { + try { + const base64 = value.replaceAll('-', '+').replaceAll('_', '/'); + const padded = `${base64}${'='.repeat((4 - (base64.length % 4)) % 4)}`; + const binary = atob(padded); + return Uint8Array.from(binary, (character) => character.charCodeAt(0)); + } catch { + throw unauthenticated(); + } +} + +function base64urlEncode(value: Uint8Array): string { + let binary = ''; + for (const byte of value) binary += String.fromCharCode(byte); + return btoa(binary) + .replaceAll('+', '-') + .replaceAll('/', '_') + .replace(/=+$/u, ''); +} + +function constantTimeHex(left: string, right: string): boolean { + if (!/^[a-f0-9]{64}$/u.test(left) || !/^[a-f0-9]{64}$/u.test(right)) + return false; + let difference = 0; + for (let index = 0; index < 64; index += 1) + difference |= left.charCodeAt(index) ^ right.charCodeAt(index); + return difference === 0; +} + +function hex(value: ArrayBuffer): string { + return [...new Uint8Array(value)] + .map((byte) => byte.toString(16).padStart(2, '0')) + .join(''); +} + +function unauthenticated(): WorkshopError { + return new WorkshopError('unauthenticated', 'Authentication required', 401); +} + +function invalidCredential(): WorkshopError { + return new WorkshopError( + 'validation_failed', + 'Local credential file is invalid', + 400, + ); +} diff --git a/src/server/authorization.ts b/src/server/authorization.ts index 1de6479..877b8b4 100644 --- a/src/server/authorization.ts +++ b/src/server/authorization.ts @@ -79,7 +79,7 @@ const MAX_CLAUSES = 64; const MAX_ATOMS_PER_CLAUSE = 64; export function authorize( - principal: WorkshopPrincipal | null, + principal: WorkshopPrincipal | AuthorizationContext | null, capability: WorkshopCapability, ): AuthorizationContext { if (!principal) { @@ -115,6 +115,7 @@ export function normalizeAuthorizationContext( 'workspaceIds', 'capabilities', 'authorizationRevision', + ...('credentialId' in value ? ['credentialId'] : []), ]); const installationId = identifier(value.installationId, 'installationId'); const principalId = identifier(value.principalId, 'principalId'); diff --git a/src/server/canonical-producer.ts b/src/server/canonical-producer.ts new file mode 100644 index 0000000..8d18555 --- /dev/null +++ b/src/server/canonical-producer.ts @@ -0,0 +1,139 @@ +import type { + ExternalOwnerTransactionAdapterV1, + RegisteredExternalProducerV1, +} from '@gnolith/taproot/search'; +import type { AuthorizationContext } from '../protocol/authorization.js'; +import { WorkshopError } from '../protocol/errors.js'; +import type { D1DatabaseLike, D1ResultLike } from './database.js'; + +export interface WorkshopSearchDomainV1 { + currentPredecessor( + sourceId: string, + context: AuthorizationContext, + ): Promise<{ eventId: string; sequence: number }>; + commitCanonicalMutation( + input: WorkshopCanonicalSearchMutationV1, + ): Promise; + adopt( + context: AuthorizationContext, + input: { cursor?: string | null; limit: number }, + ): Promise<{ enumerated: number; adopted: number; cursor: string | null }>; +} + +export interface WorkshopCanonicalSearchMutationV1 { + context: AuthorizationContext; + eventId: string; + sourceId: string; + operation: 'upsert' | 'delete'; + changeClass: string; + sourceRevision: number; + sourcePolicyRevision: number; + predecessor: { eventId: string; sequence: number } | null; + canonicalPostState: unknown; + statements: readonly { sql: string; values: readonly unknown[] }[]; +} + +export interface WorkshopCanonicalSearchMutationReceiptV1 { + eventId: string; + authorizationRevision: number; + searchGeneration: number; + results: readonly D1ResultLike[]; +} + +export interface WorkshopCanonicalOwnerAdapterControllerV1 { + readonly adapter: ExternalOwnerTransactionAdapterV1; + bind(registered: RegisteredExternalProducerV1): WorkshopSearchDomainV1; +} + +/** + * Bridges Workshop-owned SQL into Taproot's exact owner-token transaction + * boundary. Tokens are process-local identities and are consumed once. + */ +export function createWorkshopCanonicalOwnerAdapterV1( + db: D1DatabaseLike, +): WorkshopCanonicalOwnerAdapterControllerV1 { + const pending = new WeakMap(); + let bound = false; + const adapter: ExternalOwnerTransactionAdapterV1 = { + kind: 'taproot-external-owner-transaction-adapter-v1', + prepare(ownerMutationToken) { + if (!isObjectToken(ownerMutationToken)) throw invalidOwnerToken(); + const input = pending.get(ownerMutationToken); + if (!input) throw invalidOwnerToken(); + pending.delete(ownerMutationToken); + return Promise.resolve( + input.statements.map(({ sql, values }) => + db.prepare(sql).bind(...values), + ), + ); + }, + }; + + return { + adapter, + bind(registered) { + if (bound) + throw new WorkshopError( + 'validation_failed', + 'Workshop producer adapter is already bound', + 400, + ); + bound = true; + return { + async currentPredecessor(sourceId) { + const predecessor = await registered.currentPredecessor(sourceId); + if (!predecessor) + throw new WorkshopError( + 'forbidden', + 'Canonical predecessor is unavailable', + 403, + ); + return predecessor; + }, + async commitCanonicalMutation(input) { + const token = {}; + pending.set(token, input); + try { + const participant = await registered.issuer.seal({ + sourceId: input.sourceId, + sourceRevision: String(input.sourceRevision), + predecessor: input.predecessor, + ownerMutationToken: token, + }); + const receipt = await registered.commit(participant); + return { + eventId: receipt.eventId, + authorizationRevision: receipt.authorizationRevision, + searchGeneration: receipt.generation, + results: receipt.results as readonly D1ResultLike[], + }; + } finally { + pending.delete(token); + } + }, + adopt: (context, input) => registered.adopt(context, input), + }; + }, + }; +} + +export async function commitWorkshopCanonicalSearchMutationV1( + domain: WorkshopSearchDomainV1, + input: WorkshopCanonicalSearchMutationV1, +): Promise { + return domain.commitCanonicalMutation(input); +} + +function isObjectToken(value: unknown): value is object { + return ( + (typeof value === 'object' && value !== null) || typeof value === 'function' + ); +} + +function invalidOwnerToken(): WorkshopError { + return new WorkshopError( + 'validation_failed', + 'Workshop owner mutation token is invalid or already consumed', + 400, + ); +} diff --git a/src/server/config.ts b/src/server/config.ts new file mode 100644 index 0000000..22f5bb1 --- /dev/null +++ b/src/server/config.ts @@ -0,0 +1,292 @@ +import { WorkshopError } from '../protocol/errors.js'; +import type { RemoteOAuthConfig } from './auth.js'; +import { validateWorkshopAssetPrefix } from './assets.js'; + +export interface ListenerConfig { + host: string; + port: number; + profile: 'container' | 'remote' | 'test'; +} + +export interface ProtectedSelectorConfig { + kind: 'file' | 'inherited-descriptor' | 'environment-selector'; + value: string | number; +} + +export interface WorkshopServiceConfig { + format: 'gnolith-workshop-config-v1'; + listener: ListenerConfig; + authentication: + | { + profile: 'local-bearer-v1'; + credentialStore: ProtectedSelectorConfig; + } + | (RemoteOAuthConfig & { + profile: 'remote-oauth-v1'; + metadata: ProtectedSelectorConfig; + }); + adapterModule: string; + corsOrigins?: readonly string[]; + apiPrefix?: '/api/v1'; + assetMount?: { + prefix: string; + manifest: ProtectedSelectorConfig; + root: ProtectedSelectorConfig; + }; + background?: { + intervalMs: number; + timeoutMs: number; + }; + shutdownTimeoutMs?: number; +} + +export function validateWorkshopServiceConfig( + value: unknown, +): WorkshopServiceConfig { + if (!isRecord(value) || value.format !== 'gnolith-workshop-config-v1') + throw invalid('Configuration format is invalid'); + exactKeys(value, [ + 'format', + 'listener', + 'authentication', + 'adapterModule', + 'corsOrigins', + 'apiPrefix', + 'assetMount', + 'background', + 'shutdownTimeoutMs', + ]); + const listener = validateListenerConfig(value.listener); + if (!isRecord(value.authentication)) + throw invalid('Authentication configuration is required'); + const profile = value.authentication.profile; + if (profile !== 'local-bearer-v1' && profile !== 'remote-oauth-v1') + throw invalid('Authentication profile is invalid'); + if ( + typeof value.adapterModule !== 'string' || + !value.adapterModule || + value.adapterModule.includes('\u0000') + ) + throw invalid('adapterModule is invalid'); + if (value.apiPrefix !== undefined && value.apiPrefix !== '/api/v1') + throw invalid('apiPrefix is invalid'); + const corsOrigins = value.corsOrigins; + if ( + corsOrigins !== undefined && + (!Array.isArray(corsOrigins) || + corsOrigins.some( + (origin) => + typeof origin !== 'string' || + new URL(origin).origin !== origin || + !['https:', 'http:'].includes(new URL(origin).protocol), + )) + ) + throw invalid('CORS origins are invalid'); + const authentication = + profile === 'local-bearer-v1' + ? localAuthentication(value.authentication) + : remoteAuthentication(value.authentication); + const background = value.background; + if (isRecord(background)) exactKeys(background, ['intervalMs', 'timeoutMs']); + if ( + background !== undefined && + (!isRecord(background) || + !boundedInteger(background.intervalMs, 100, 86_400_000) || + !boundedInteger(background.timeoutMs, 1, 86_400_000) || + Number(background.timeoutMs) > Number(background.intervalMs)) + ) + throw invalid('Background trigger configuration is invalid'); + const shutdownTimeoutMs = + value.shutdownTimeoutMs === undefined ? 30_000 : value.shutdownTimeoutMs; + if (!boundedInteger(shutdownTimeoutMs, 1, 300_000)) + throw invalid('Shutdown timeout is invalid'); + const assetMount = + value.assetMount === undefined + ? undefined + : validateAssetMount(value.assetMount); + return { + format: 'gnolith-workshop-config-v1', + listener, + authentication, + adapterModule: value.adapterModule, + ...(corsOrigins ? { corsOrigins: [...(corsOrigins as string[])] } : {}), + apiPrefix: '/api/v1', + ...(assetMount ? { assetMount } : {}), + ...(background + ? { + background: { + intervalMs: Number(background.intervalMs), + timeoutMs: Number(background.timeoutMs), + }, + } + : {}), + shutdownTimeoutMs: Number(shutdownTimeoutMs), + }; +} + +export function validateListenerConfig(value: unknown): ListenerConfig { + if ( + !isRecord(value) || + typeof value.host !== 'string' || + !value.host || + value.host.length > 255 || + !boundedInteger(value.port, 0, 65_535) || + !['container', 'remote', 'test'].includes(String(value.profile)) + ) + throw invalid('Listener configuration is invalid'); + exactKeys(value, ['host', 'port', 'profile']); + const profile = value.profile as ListenerConfig['profile']; + const host = value.host; + const port = Number(value.port); + if (profile === 'container' && (isLoopback(host) || port === 0)) + throw invalid( + 'Container profile requires an explicit non-loopback container-network bind', + ); + if (profile === 'remote' && port === 0) + throw invalid('Remote profile requires an explicit port'); + return { host, port, profile }; +} + +function localAuthentication( + value: Record, +): WorkshopServiceConfig['authentication'] { + exactKeys(value, ['profile', 'credentialStore']); + return { + profile: 'local-bearer-v1', + credentialStore: selector(value.credentialStore), + }; +} + +function remoteAuthentication( + value: Record, +): WorkshopServiceConfig['authentication'] { + const required = [ + 'issuer', + 'audience', + 'resource', + 'principalClaim', + 'workspaceClaim', + 'installationClaim', + 'credentialClaim', + ]; + if (required.some((key) => typeof value[key] !== 'string')) + throw invalid('Remote OAuth configuration is invalid'); + if (!boundedInteger(value.maxClockSkewSeconds, 0, 300)) + throw invalid('Remote OAuth clock skew is invalid'); + exactKeys(value, [ + 'profile', + ...required, + 'algorithms', + 'requiredScopes', + 'authorizationServers', + 'scopeCapabilityMap', + 'maxClockSkewSeconds', + 'metadata', + ]); + return { + profile: 'remote-oauth-v1', + issuer: String(value.issuer), + audience: String(value.audience), + algorithms: stringArray(value.algorithms), + requiredScopes: stringArray(value.requiredScopes), + resource: String(value.resource), + authorizationServers: stringArray(value.authorizationServers), + scopeCapabilityMap: stringMap(value.scopeCapabilityMap), + principalClaim: String(value.principalClaim), + workspaceClaim: String(value.workspaceClaim), + installationClaim: String(value.installationClaim), + credentialClaim: String(value.credentialClaim), + maxClockSkewSeconds: Number(value.maxClockSkewSeconds), + metadata: selector(value.metadata), + }; +} + +function validateAssetMount( + value: unknown, +): NonNullable { + if (!isRecord(value) || typeof value.prefix !== 'string') + throw invalid('Asset mount configuration is invalid'); + exactKeys(value, ['prefix', 'manifest', 'root']); + return { + prefix: validateWorkshopAssetPrefix(value.prefix), + manifest: selector(value.manifest), + root: selector(value.root), + }; +} + +function selector(value: unknown): ProtectedSelectorConfig { + if ( + !isRecord(value) || + !['file', 'inherited-descriptor', 'environment-selector'].includes( + String(value.kind), + ) || + (typeof value.value !== 'string' && typeof value.value !== 'number') + ) + throw invalid('Protected selector is invalid'); + exactKeys(value, ['kind', 'value']); + return { + kind: value.kind as ProtectedSelectorConfig['kind'], + value: value.value, + }; +} + +function stringArray(value: unknown): string[] { + if ( + !Array.isArray(value) || + value.length === 0 || + value.length > 256 || + value.some( + (entry) => typeof entry !== 'string' || !entry || entry.length > 4096, + ) + ) + throw invalid('Expected a bounded nonempty string array'); + return [...new Set(value as string[])].sort(); +} + +function stringMap(value: unknown): Record { + if (!isRecord(value)) throw invalid('Expected a string map'); + const output: Record = {}; + for (const [key, entry] of Object.entries(value)) { + if (!key || typeof entry !== 'string' || !entry) + throw invalid('Expected a string map'); + output[key] = entry; + } + return output; +} + +function isLoopback(host: string): boolean { + return ( + host === 'localhost' || + host === '::1' || + /^127(?:\.[0-9]{1,3}){3}$/u.test(host) + ); +} + +function boundedInteger( + value: unknown, + minimum: number, + maximum: number, +): boolean { + return ( + Number.isSafeInteger(value) && + Number(value) >= minimum && + Number(value) <= maximum + ); +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function exactKeys( + value: Record, + allowed: readonly string[], +): void { + const accepted = new Set(allowed); + if (Object.keys(value).some((key) => !accepted.has(key))) + throw invalid('Configuration contains unknown fields'); +} + +function invalid(message: string): WorkshopError { + return new WorkshopError('validation_failed', message, 400); +} diff --git a/src/server/health.ts b/src/server/health.ts index 338a5e5..15148ad 100644 --- a/src/server/health.ts +++ b/src/server/health.ts @@ -111,7 +111,7 @@ export class HealthService { ]); const compatibility = installedPackageVersion !== null && - /^0\.4\./u.test(installedPackageVersion) && + /^0\.5\./u.test(installedPackageVersion) && hostCompatibility; const persistence = d1; const checks = { @@ -127,7 +127,7 @@ export class HealthService { return { status: Object.values(checks).every(Boolean) ? 'ok' : 'degraded', schemaVersion, - workshopVersion: '0.4.2', + workshopVersion: '0.5.0', checks, tables, indexes, diff --git a/src/server/idempotency.ts b/src/server/idempotency.ts new file mode 100644 index 0000000..64d292c --- /dev/null +++ b/src/server/idempotency.ts @@ -0,0 +1,185 @@ +import { WorkshopError } from '../protocol/errors.js'; +import type { D1DatabaseLike } from './database.js'; +import { requiredText } from './validation.js'; + +export type WorkshopReceiptOperation = + | 'memory.upsert' + | 'memory.delete' + | 'prompt.create' + | 'prompt.update' + | 'prompt.delete'; + +export interface WorkshopOperationReceiptBinding { + installationId: string; + principalId: string; + operation: WorkshopReceiptOperation; + idempotencyKey: string; +} + +export interface WorkshopPreparedOperationReceipt { + requestDigest: string; + replay: { subjectId: string; response: unknown } | null; +} + +export async function prepareWorkshopOperationReceipt( + db: D1DatabaseLike, + binding: WorkshopOperationReceiptBinding, + request: unknown, +): Promise { + const idempotencyKey = requiredText( + binding.idempotencyKey, + 'idempotencyKey', + 256, + ); + const requestDigest = await digestWorkshopOperationRequest(request); + const row = await db + .prepare( + `SELECT subject_id, request_digest, response_json + FROM workshop_operation_receipts + WHERE installation_id = ? AND principal_id = ? + AND operation = ? AND idempotency_key = ?`, + ) + .bind( + binding.installationId, + binding.principalId, + binding.operation, + idempotencyKey, + ) + .first<{ + subject_id: string; + request_digest: string; + response_json: string; + }>(); + if (!row) return { requestDigest, replay: null }; + if (row.request_digest !== requestDigest) + throw new WorkshopError( + 'conflict', + 'Idempotency key was used for another mutation', + 409, + ); + let response: unknown; + try { + response = JSON.parse(row.response_json); + } catch { + throw new WorkshopError( + 'internal_error', + 'Idempotency receipt is invalid', + 500, + ); + } + return { + requestDigest, + replay: { subjectId: row.subject_id, response }, + }; +} + +export async function waitForWorkshopOperationReceipt( + db: D1DatabaseLike, + binding: WorkshopOperationReceiptBinding, + request: unknown, +): Promise { + let prepared = await prepareWorkshopOperationReceipt(db, binding, request); + for (let attempt = 0; !prepared.replay && attempt < 8; attempt += 1) { + await new Promise((resolve) => { + setTimeout(resolve, 5 * (attempt + 1)); + }); + prepared = await prepareWorkshopOperationReceipt(db, binding, request); + } + return prepared; +} + +export function workshopOperationReceiptStatement( + binding: WorkshopOperationReceiptBinding, + requestDigest: string, + subjectId: string, + response: unknown, + createdAt: string, + resultingRevision: number, + eventId: string, +): { sql: string; values: readonly unknown[] } { + const successfulMutation = + binding.operation === 'memory.delete' || + binding.operation === 'memory.upsert' + ? `SELECT 1 FROM workshop_memories + WHERE installation_id = ? AND slug = ? AND revision = ? + AND search_event_id = ?` + : `SELECT 1 FROM workshop_prompts + WHERE installation_id = ? AND id = ? AND revision = ? + AND search_event_id = ?`; + return { + sql: `INSERT INTO workshop_operation_receipts + (installation_id, principal_id, operation, idempotency_key, + subject_id, request_digest, response_json, created_at) + SELECT ?, ?, ?, ?, ?, ?, ?, ? + WHERE EXISTS (${successfulMutation})`, + values: [ + binding.installationId, + binding.principalId, + binding.operation, + binding.idempotencyKey, + subjectId, + requestDigest, + JSON.stringify(response), + createdAt, + binding.installationId, + subjectId, + resultingRevision, + eventId, + ], + }; +} + +export async function digestWorkshopOperationRequest( + request: unknown, +): Promise { + return sha256(stableJson(request)); +} + +function stableJson(value: unknown): string { + const seen = new Set(); + const normalize = (entry: unknown): unknown => { + if ( + entry === null || + typeof entry === 'string' || + typeof entry === 'boolean' + ) + return entry; + if (typeof entry === 'number') { + if (!Number.isFinite(entry)) + throw new WorkshopError( + 'validation_failed', + 'Idempotent mutation input is invalid', + 400, + ); + return entry; + } + if (Array.isArray(entry)) return entry.map(normalize); + if (typeof entry !== 'object') + throw new WorkshopError( + 'validation_failed', + 'Idempotent mutation input is invalid', + 400, + ); + if (seen.has(entry)) + throw new WorkshopError( + 'validation_failed', + 'Idempotent mutation input is cyclic', + 400, + ); + seen.add(entry); + const output: Record = {}; + for (const key of Object.keys(entry).sort()) { + const child = (entry as Record)[key]; + if (child !== undefined) output[key] = normalize(child); + } + seen.delete(entry); + return output; + }; + return JSON.stringify(normalize(value)); +} + +async function sha256(value: string): Promise { + return Buffer.from( + await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)), + ).toString('hex'); +} diff --git a/src/server/lifecycle.ts b/src/server/lifecycle.ts new file mode 100644 index 0000000..575cb0f --- /dev/null +++ b/src/server/lifecycle.ts @@ -0,0 +1,102 @@ +import { WorkshopError } from '../protocol/errors.js'; + +export interface BackgroundTrigger { + run(signal: AbortSignal): Promise; + checkpoint(signal: AbortSignal): Promise; +} + +export interface BoundedTriggerOptions { + trigger: BackgroundTrigger; + intervalMs: number; + timeoutMs: number; + onError?: (error: unknown) => void | Promise; +} + +export interface BoundedTriggerController { + readonly running: boolean; + start(): void; + invoke(): Promise; + stop(signal?: AbortSignal): Promise; +} + +export function createBoundedTrigger( + options: BoundedTriggerOptions, +): BoundedTriggerController { + if ( + !Number.isSafeInteger(options.intervalMs) || + options.intervalMs < 100 || + !Number.isSafeInteger(options.timeoutMs) || + options.timeoutMs < 1 || + options.timeoutMs > options.intervalMs + ) + throw new WorkshopError( + 'validation_failed', + 'Background trigger bounds are invalid', + 400, + ); + let timer: ReturnType | undefined; + let inFlight: Promise | undefined; + let running = false; + let stopping = false; + const abort = new AbortController(); + const invoke = async (): Promise => { + if (stopping || inFlight) return false; + const timeout = new AbortController(); + const timeoutId = setTimeout( + () => timeout.abort(new Error('background trigger timeout')), + options.timeoutMs, + ); + const signal = AbortSignal.any([abort.signal, timeout.signal]); + inFlight = options.trigger + .run(signal) + .catch(async (error) => { + try { + await options.onError?.(error); + } catch { + // Trigger error telemetry never changes lifecycle behavior. + } + }) + .finally(() => { + clearTimeout(timeoutId); + inFlight = undefined; + }); + await inFlight; + return true; + }; + return { + get running() { + return running; + }, + start() { + if (running || stopping) return; + running = true; + timer = setInterval(() => void invoke(), options.intervalMs); + timer.unref?.(); + }, + invoke, + async stop(signal = new AbortController().signal) { + if (stopping) { + await inFlight; + return; + } + stopping = true; + running = false; + if (timer) clearInterval(timer); + abort.abort(new Error('service stopping')); + await Promise.race([inFlight ?? Promise.resolve(), aborted(signal)]); + await options.trigger.checkpoint(signal); + }, + }; +} + +function aborted(signal: AbortSignal): Promise { + return new Promise((_, reject) => { + if (signal.aborted) { + reject(signal.reason); + return; + } + signal.addEventListener('abort', () => reject(signal.reason), { + once: true, + }); + }); +} diff --git a/src/server/memories.ts b/src/server/memories.ts index 01a0117..a9aacc8 100644 --- a/src/server/memories.ts +++ b/src/server/memories.ts @@ -38,7 +38,12 @@ import { import { commitWorkshopCanonicalSearchMutationV1, type WorkshopSearchDomainV1, -} from './search.js'; +} from './canonical-producer.js'; +import { + prepareWorkshopOperationReceipt, + waitForWorkshopOperationReceipt, + workshopOperationReceiptStatement, +} from './idempotency.js'; import { isoDate, memorySlug, @@ -271,7 +276,7 @@ export class MemoryService { input: UpsertMemoryInput, authorization: AuthorizationContext, ): Promise { - const context = await this.#context(authorization, 'memory-write'); + const context = await this.#context(authorization, 'memory:write'); const workspaceId = requireActiveWorkspace(context); const slug = memorySlug(slugValue); const description = requiredText( @@ -462,10 +467,9 @@ export class MemoryService { authorization: AuthorizationContext, idempotencyKey: string, ): Promise { - const context = await this.#context(authorization, 'memory-write'); + const context = await this.#context(authorization, 'memory:write'); const key = requiredText(idempotencyKey, 'idempotencyKey', 256); const workspaceId = requireActiveWorkspace(context); - const storedIdempotencyKey = `${workspaceId}\u0000${context.principalId}\u0000${key}`; const slug = memorySlug(slugValue); const description = requiredText( input.description, @@ -477,10 +481,73 @@ export class MemoryService { 'content', this.#limits.maxMemoryBytes, ); - const scope = normalizeVisibilityScope( - input.visibility ?? defaultVisibility(context), + const title = + input.title === undefined + ? undefined + : requiredText(input.title, 'title', this.#limits.maxDescriptionBytes); + const applicability = + input.applicability === undefined + ? undefined + : jsonObject( + input.applicability, + 'applicability', + this.#limits.maxDescriptionBytes, + ); + const provenance = + input.provenance === undefined + ? undefined + : jsonObject( + input.provenance, + 'provenance', + this.#limits.maxDescriptionBytes, + ); + const language = + input.language === undefined + ? undefined + : requiredText(input.language, 'language', 64); + const attribution = + input.attribution === undefined + ? undefined + : jsonObject( + input.attribution, + 'attribution', + this.#limits.maxDescriptionBytes, + ); + const requestedScope = + input.visibility === undefined + ? undefined + : normalizeVisibilityScope(input.visibility); + const expected = revisionPrecondition(input); + const binding = { + installationId: context.installationId, + principalId: context.principalId, + operation: 'memory.upsert' as const, + idempotencyKey: key, + }; + const request = { + slug, + description, + content, + ...(title === undefined ? {} : { title }), + ...(applicability === undefined ? {} : { applicability }), + ...(provenance === undefined ? {} : { provenance }), + ...(language === undefined ? {} : { language }), + ...(attribution === undefined ? {} : { attribution }), + ...(requestedScope === undefined ? {} : { visibility: requestedScope }), + ...(input.expectedRevision === undefined + ? {} + : { expectedRevision: input.expectedRevision }), + ...(input.expectedUpdatedAt === undefined + ? {} + : { expectedUpdatedAt: input.expectedUpdatedAt }), + }; + const prepared = await prepareWorkshopOperationReceipt( + this.db, + binding, + request, ); - const now = this.#clock().toISOString(); + if (prepared.replay) + return authorizedReceiptMemory(prepared.replay, context, slug); return observed( this.options.observe, { @@ -488,130 +555,160 @@ export class MemoryService { principalId: context.principalId, }, async () => { - const visibility = visibilitySql('workshop_memories', context); - const existing = await this.db + const predicate = visibilitySql('workshop_memories', context); + const row = await this.db .prepare( `SELECT * FROM workshop_memories - WHERE idempotency_key = ? AND deleted_at IS NULL AND ${visibility.sql}`, - ) - .bind(storedIdempotencyKey, ...visibility.values) - .first(); - if (existing) { - if ( - existing.slug === slug && - existing.description === description && - existing.content === content && - existing.visibility_scope === serializeVisibilityScope(scope) - ) { - await this.#recheck(existing, context); - return toMemory(existing); - } - throw new WorkshopError( - 'conflict', - `Idempotent memory input conflicts with existing content`, - ); - } - const occupied = await this.db - .prepare( - `SELECT slug FROM workshop_memories WHERE installation_id = ? AND slug = ? AND deleted_at IS NULL - AND ${visibility.sql}`, + AND ${predicate.sql}`, ) - .bind(context.installationId, slug, ...visibility.values) - .first<{ slug: string }>(); - if (occupied) - throw new WorkshopError( - 'conflict', - `Idempotent memory input conflicts with existing content`, - ); + .bind(context.installationId, slug, ...predicate.values) + .first(); + if (expected && !row) throw denied(); + if ( + requestedScope && + row && + row.owner_principal_id !== context.principalId && + !context.capabilities.includes('admin') + ) + throw denied(); + const current = row ? toMemory(row) : null; + if ( + expected && + current && + (expected.column === 'revision' + ? current.revision !== expected.value + : current.updatedAt !== expected.value) + ) + throw new WorkshopError('conflict', 'Memory state changed'); + const scope = + requestedScope ?? current?.visibility ?? defaultVisibility(context); + const policyChanged = current + ? serializeVisibilityScope(scope) !== + serializeVisibilityScope(current.visibility) + : false; + const now = this.#clock().toISOString(); const eventId = crypto.randomUUID(); const next: Memory = { slug, - title: input.title ?? description, + title: title ?? current?.title ?? description, description, content, - applicability: input.applicability ?? {}, - provenance: input.provenance ?? {}, - language: input.language ?? 'en', - attribution: input.attribution ?? {}, - createdAt: now, - updatedAt: now, - revision: 1, - policyRevision: 1, + applicability: applicability ?? current?.applicability ?? {}, + provenance: provenance ?? current?.provenance ?? {}, + language: language ?? current?.language ?? 'en', + attribution: attribution ?? current?.attribution ?? {}, + createdAt: current?.createdAt ?? now, + updatedAt: current ? nextTimestamp(now, current.updatedAt) : now, + revision: (current?.revision ?? 0) + 1, + policyRevision: current + ? current.policyRevision + (policyChanged ? 1 : 0) + : 1, installationId: context.installationId, - ownerPrincipalId: context.principalId, - workspaceId, + ownerPrincipalId: current?.ownerPrincipalId ?? context.principalId, + workspaceId: current?.workspaceId ?? workspaceId, visibility: scope, authorizationRevision: context.authorizationRevision, }; - let inserted: MemoryRow | null; + const receiptStatement = workshopOperationReceiptStatement( + binding, + prepared.requestDigest, + slug, + next, + now, + next.revision, + eventId, + ); + let committed: MemoryRow | null; try { - inserted = await this.#commitCanonical( - context, - next, - 'memory.create', - null, - { - sql: `INSERT INTO workshop_memories - (slug, idempotency_key, title, description, content, applicability_json, - provenance_json, language, attribution_json, created_at, updated_at, revision, - policy_revision, installation_id, owner_principal_id, workspace_id, visibility_scope, - authorization_revision, search_event_id, search_event_sequence) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?, ?, ?, ?, ?, ?, NULL) RETURNING *`, - values: [ - slug, - storedIdempotencyKey, - next.title, - description, - content, - JSON.stringify(next.applicability), - JSON.stringify(next.provenance), - next.language, - JSON.stringify(next.attribution), - now, - now, - context.installationId, - context.principalId, - workspaceId, - serializeVisibilityScope(scope), - context.authorizationRevision, + committed = current + ? await this.#commitCanonical( + context, + next, + policyChanged ? 'memory.policy' : 'memory.update', + await this.#predecessor(slug, context), + { + sql: `UPDATE workshop_memories SET title = ?, description = ?, content = ?, + applicability_json = ?, provenance_json = ?, language = ?, attribution_json = ?, + visibility_scope = ?, authorization_revision = ?, policy_revision = ?, revision = ?, + updated_at = ?, search_event_id = ?, search_event_sequence = NULL + WHERE installation_id = ? AND slug = ? AND revision = ? AND deleted_at IS NULL + ${expected ? `AND ${expected.column} = ?` : ''} AND ${predicate.sql} + RETURNING *`, + values: [ + next.title, + next.description, + next.content, + JSON.stringify(next.applicability), + JSON.stringify(next.provenance), + next.language, + JSON.stringify(next.attribution), + serializeVisibilityScope(scope), + context.authorizationRevision, + next.policyRevision, + next.revision, + next.updatedAt, + eventId, + context.installationId, + slug, + current.revision, + ...(expected ? [expected.value] : []), + ...predicate.values, + ], + }, eventId, - ], - }, - eventId, - ); + [receiptStatement], + ) + : await this.#commitCanonical( + context, + next, + 'memory.create', + null, + { + sql: `INSERT INTO workshop_memories + (slug, idempotency_key, title, description, content, applicability_json, + provenance_json, language, attribution_json, created_at, updated_at, revision, + policy_revision, installation_id, owner_principal_id, workspace_id, + visibility_scope, authorization_revision, search_event_id, search_event_sequence) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?, ?, ?, ?, ?, ?, NULL) + RETURNING *`, + values: [ + slug, + `${workspaceId}\u0000${context.principalId}\u0000${key}`, + next.title, + description, + content, + JSON.stringify(next.applicability), + JSON.stringify(next.provenance), + next.language, + JSON.stringify(next.attribution), + now, + now, + context.installationId, + context.principalId, + workspaceId, + serializeVisibilityScope(scope), + context.authorizationRevision, + eventId, + ], + }, + eventId, + [receiptStatement], + ); } catch (error) { - if ( - !(error instanceof Error) || - !/UNIQUE constraint failed/iu.test(error.message) - ) - throw error; - inserted = null; - } - if (inserted) { - authorizeRecord(toAuthorizationRecord(inserted), context); - return toMemory(inserted); - } - const raced = await this.db - .prepare( - `SELECT * FROM workshop_memories - WHERE idempotency_key = ? AND ${visibility.sql}`, - ) - .bind(storedIdempotencyKey, ...visibility.values) - .first(); - if ( - raced?.slug === slug && - raced.description === description && - raced.content === content && - raced.visibility_scope === serializeVisibilityScope(scope) - ) { - await this.#recheck(raced, context); - return toMemory(raced); + const raced = await waitForWorkshopOperationReceipt( + this.db, + binding, + request, + ); + if (raced.replay) + return authorizedReceiptMemory(raced.replay, context, slug); + throw error; } - throw new WorkshopError( - 'conflict', - `Idempotent memory input conflicts with existing content`, - ); + if (!committed) + throw new WorkshopError('conflict', 'Memory state changed', 409); + authorizeRecord(toAuthorizationRecord(committed), context); + return toMemory(committed); }, ); } @@ -620,11 +717,30 @@ export class MemoryService { slugValue: string, expectedRevision: number, authorization: AuthorizationContext, + idempotencyKey?: string, ): Promise { - const context = await this.#context(authorization, 'memory-write'); + const context = await this.#context(authorization, 'memory:write'); if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 1) throw validation('expectedRevision must be a positive safe integer'); const slug = memorySlug(slugValue); + const binding = idempotencyKey + ? { + installationId: context.installationId, + principalId: context.principalId, + operation: 'memory.delete' as const, + idempotencyKey, + } + : undefined; + const prepared = binding + ? await prepareWorkshopOperationReceipt(this.db, binding, { + slug, + expectedRevision, + }) + : undefined; + if (prepared?.replay) { + authorizedReceiptMemory(prepared.replay, context, slug); + return; + } const predicate = visibilitySql('workshop_memories', context); const row = await this.db .prepare( @@ -680,9 +796,34 @@ export class MemoryService { eventId, updatedAt, ), + ...(binding && prepared + ? [ + workshopOperationReceiptStatement( + binding, + prepared.requestDigest, + slug, + deleted, + updatedAt, + deleted.revision, + eventId, + ), + ] + : []), ], }, - ); + ).catch(async (error: unknown) => { + if (binding) { + const raced = await waitForWorkshopOperationReceipt(this.db, binding, { + slug, + expectedRevision, + }); + if (raced.replay) { + authorizedReceiptMemory(raced.replay, context, slug); + return { results: [{ meta: { changes: 1 } }] }; + } + } + throw error; + }); if ( (receipt.results[0]?.meta as { changes?: number } | undefined) ?.changes === 0 @@ -761,16 +902,7 @@ export class MemoryService { slug: string, context: AuthorizationContext, ): Promise<{ eventId: string; sequence: number }> { - const row = await this.db - .prepare( - `SELECT current_event_id AS event_id, current_event_sequence AS sequence - FROM taproot_unified_search_source_registry - WHERE installation_id = ? AND source_kind = 'memory' AND source_id = ?`, - ) - .bind(context.installationId, slug) - .first<{ event_id: string; sequence: number }>(); - if (!row || !Number.isSafeInteger(Number(row.sequence))) throw denied(); - return { eventId: row.event_id, sequence: Number(row.sequence) }; + return this.#search().currentPredecessor(slug, context); } #revisionStatement( @@ -782,7 +914,12 @@ export class MemoryService { return { sql: `INSERT INTO workshop_memory_revisions (installation_id, memory_id, revision, snapshot_json, actor_principal_id, event_id, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?)`, + SELECT ?, ?, ?, ?, ?, ?, ? + WHERE EXISTS ( + SELECT 1 FROM workshop_memories + WHERE installation_id = ? AND slug = ? AND revision = ? + AND search_event_id = ? + )`, values: [ memory.installationId, memory.slug, @@ -791,6 +928,10 @@ export class MemoryService { actor, eventId, now, + memory.installationId, + memory.slug, + memory.revision, + eventId, ], }; } @@ -802,6 +943,10 @@ export class MemoryService { predecessor: { eventId: string; sequence: number } | null, mutation: { sql: string; values: readonly unknown[] }, eventId: string, + additionalStatements: readonly { + sql: string; + values: readonly unknown[]; + }[] = [], ): Promise { const receipt = await commitWorkshopCanonicalSearchMutationV1( this.#search(), @@ -823,6 +968,7 @@ export class MemoryService { eventId, next.updatedAt, ), + ...additionalStatements, ], }, ); @@ -831,7 +977,7 @@ export class MemoryService { async #context( authorization: AuthorizationContext, - capability: 'read' | 'memory-write' = 'read', + capability: 'read' | 'memory:write' = 'read', ): Promise { const context = await requireCurrentAuthorization( this.options.authorization, @@ -948,6 +1094,30 @@ function parseMemoryRevision( return memory; } +function authorizedReceiptMemory( + replay: { subjectId: string; response: unknown }, + context: AuthorizationContext, + expectedSlug: string, +): Memory { + if ( + replay.subjectId !== expectedSlug || + (replay.response as { slug?: unknown } | null)?.slug !== expectedSlug + ) + throw new WorkshopError( + 'conflict', + 'Idempotency key was used for another mutation', + 409, + ); + const raw = replay.response as { revision?: unknown } | null; + const memory = parseMemoryRevision( + JSON.stringify(replay.response), + expectedSlug, + Number(raw?.revision), + ); + authorizeRecord(memory, context); + return memory; +} + function toAuthorizationRecord( row: Pick< MemoryRow, diff --git a/src/server/prompts.ts b/src/server/prompts.ts index c740bc3..874c2fa 100644 --- a/src/server/prompts.ts +++ b/src/server/prompts.ts @@ -38,7 +38,13 @@ import { import { commitWorkshopCanonicalSearchMutationV1, type WorkshopSearchDomainV1, -} from './search.js'; +} from './canonical-producer.js'; +import { + prepareWorkshopOperationReceipt, + waitForWorkshopOperationReceipt, + workshopOperationReceiptStatement, + type WorkshopOperationReceiptBinding, +} from './idempotency.js'; import { pageLimit, requiredText, validation } from './validation.js'; interface PromptRow { @@ -234,8 +240,17 @@ export class PromptService { async create( input: CreatePromptInput, authorization: AuthorizationContext, + idempotencyKey?: string, ): Promise { - const context = await this.#context(authorization, 'prompt-write'); + const context = await this.#context(authorization, 'prompt:write'); + const binding = idempotencyKey + ? receiptBinding(context, 'prompt.create', idempotencyKey) + : undefined; + const prepared = binding + ? await prepareWorkshopOperationReceipt(this.db, binding, { input }) + : undefined; + if (prepared?.replay) + return authorizedReceiptPrompt(prepared.replay, context); const workspaceId = requireActiveWorkspace(context); const now = this.#clock().toISOString(); const id = input.id ? requiredText(input.id, 'id', 256) : this.#createId(); @@ -294,6 +309,19 @@ export class PromptService { ], }, revisionStatement(prompt, context.principalId, eventId, now), + ...(binding && prepared + ? [ + workshopOperationReceiptStatement( + binding, + prepared.requestDigest, + prompt.id, + prompt, + now, + prompt.revision, + eventId, + ), + ] + : []), ]; try { const receipt = await commitWorkshopCanonicalSearchMutationV1( @@ -316,11 +344,18 @@ export class PromptService { throw new WorkshopError('internal_error', 'Prompt creation failed'); return toPrompt(row); } catch (error) { + if (binding) { + const raced = await waitForWorkshopOperationReceipt(this.db, binding, { + input, + }); + if (raced.replay) return authorizedReceiptPrompt(raced.replay, context); + } if ( error instanceof Error && /UNIQUE constraint failed/iu.test(error.message) - ) + ) { throw new WorkshopError('conflict', 'Prompt id or name already exists'); + } throw error; } } @@ -329,9 +364,18 @@ export class PromptService { id: string, input: UpdatePromptInput, authorization: AuthorizationContext, + idempotencyKey?: string, ): Promise { - const context = await this.#context(authorization, 'prompt-write'); + const context = await this.#context(authorization, 'prompt:write'); validateRevision(input.expectedRevision); + const binding = idempotencyKey + ? receiptBinding(context, 'prompt.update', idempotencyKey) + : undefined; + const prepared = binding + ? await prepareWorkshopOperationReceipt(this.db, binding, { id, input }) + : undefined; + if (prepared?.replay) + return authorizedReceiptPrompt(prepared.replay, context, id); const current = await this.#getForMutation(id, context); if (current.revision !== input.expectedRevision) throw new WorkshopError('conflict', 'Prompt state changed'); @@ -423,22 +467,48 @@ export class PromptService { ], }, revisionStatement(next, context.principalId, eventId, next.updatedAt), + ...(binding && prepared + ? [ + workshopOperationReceiptStatement( + binding, + prepared.requestDigest, + id, + next, + next.updatedAt, + next.revision, + eventId, + ), + ] + : []), ]; - const receipt = await commitWorkshopCanonicalSearchMutationV1( - this.options.search, - { - context, - eventId, - sourceId: id, - operation: 'upsert', - changeClass: policyChanged ? 'prompt.policy' : 'prompt.update', - sourceRevision: next.revision, - sourcePolicyRevision: next.policyRevision, - predecessor: sequence, - canonicalPostState: next, - statements, - }, - ); + let receipt; + try { + receipt = await commitWorkshopCanonicalSearchMutationV1( + this.options.search, + { + context, + eventId, + sourceId: id, + operation: 'upsert', + changeClass: policyChanged ? 'prompt.policy' : 'prompt.update', + sourceRevision: next.revision, + sourcePolicyRevision: next.policyRevision, + predecessor: sequence, + canonicalPostState: next, + statements, + }, + ); + } catch (error) { + if (binding) { + const raced = await waitForWorkshopOperationReceipt(this.db, binding, { + id, + input, + }); + if (raced.replay) + return authorizedReceiptPrompt(raced.replay, context, id); + } + throw error; + } const row = receipt.results[0]?.results[0] as PromptRow | undefined; if (!row) throw new WorkshopError('conflict', 'Prompt state changed'); return toPrompt(row); @@ -456,9 +526,23 @@ export class PromptService { id: string, expectedRevision: number, authorization: AuthorizationContext, + idempotencyKey?: string, ): Promise { - const context = await this.#context(authorization, 'prompt-write'); + const context = await this.#context(authorization, 'prompt:write'); validateRevision(expectedRevision); + const binding = idempotencyKey + ? receiptBinding(context, 'prompt.delete', idempotencyKey) + : undefined; + const prepared = binding + ? await prepareWorkshopOperationReceipt(this.db, binding, { + id, + expectedRevision, + }) + : undefined; + if (prepared?.replay) { + authorizedReceiptPrompt(prepared.replay, context, id); + return; + } const current = await this.#getForMutation(id, context); if (current.revision !== expectedRevision) throw new WorkshopError('conflict', 'Prompt state changed'); @@ -505,9 +589,34 @@ export class PromptService { ], }, revisionStatement(deleted, context.principalId, eventId, updatedAt), + ...(binding && prepared + ? [ + workshopOperationReceiptStatement( + binding, + prepared.requestDigest, + id, + deleted, + updatedAt, + deleted.revision, + eventId, + ), + ] + : []), ], }, - ); + ).catch(async (error: unknown) => { + if (binding) { + const raced = await waitForWorkshopOperationReceipt(this.db, binding, { + id, + expectedRevision, + }); + if (raced.replay) { + authorizedReceiptPrompt(raced.replay, context, id); + return { results: [{ meta: { changes: 1 } }] }; + } + } + throw error; + }); if ( (receipt.results[0]?.meta as { changes?: number } | undefined) ?.changes === 0 @@ -563,21 +672,7 @@ export class PromptService { id: string, context: AuthorizationContext, ): Promise<{ eventId: string; sequence: number }> { - const row = await this.db - .prepare( - `SELECT current_event_id AS search_event_id, - current_event_sequence AS search_event_sequence - FROM taproot_unified_search_source_registry - WHERE installation_id = ? AND source_kind = 'prompt' AND source_id = ?`, - ) - .bind(context.installationId, id) - .first>(); - if (!row?.search_event_id || row.search_event_sequence === null) - throw denied(); - return { - eventId: row.search_event_id, - sequence: row.search_event_sequence, - }; + return this.options.search.currentPredecessor(id, context); } async #row(idValue: string, context: AuthorizationContext) { @@ -626,7 +721,7 @@ export class PromptService { async #context( authorization: AuthorizationContext, - capability: 'read' | 'prompt-write' = 'read', + capability: 'read' | 'prompt:write' = 'read', ): Promise { const context = await requireCurrentAuthorization( this.options.authorization, @@ -693,7 +788,12 @@ function revisionStatement( return { sql: `INSERT INTO workshop_prompt_revisions (installation_id, prompt_id, revision, snapshot_json, actor_principal_id, event_id, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?)`, + SELECT ?, ?, ?, ?, ?, ?, ? + WHERE EXISTS ( + SELECT 1 FROM workshop_prompts + WHERE installation_id = ? AND id = ? AND revision = ? + AND search_event_id = ? + )`, values: [ prompt.installationId, prompt.id, @@ -702,6 +802,10 @@ function revisionStatement( actor, eventId, now, + prompt.installationId, + prompt.id, + prompt.revision, + eventId, ], }; } @@ -765,6 +869,44 @@ function parsePromptRevision( return prompt; } +function receiptBinding( + context: AuthorizationContext, + operation: WorkshopOperationReceiptBinding['operation'], + idempotencyKey: string, +): WorkshopOperationReceiptBinding { + return { + installationId: context.installationId, + principalId: context.principalId, + operation, + idempotencyKey, + }; +} + +function authorizedReceiptPrompt( + replay: { subjectId: string; response: unknown }, + context: AuthorizationContext, + expectedId?: string, +): Prompt { + if ( + expectedId !== undefined && + (replay.subjectId !== expectedId || + (replay.response as { id?: unknown } | null)?.id !== expectedId) + ) + throw new WorkshopError( + 'conflict', + 'Idempotency key was used for another mutation', + 409, + ); + const raw = replay.response as { revision?: unknown } | null; + const prompt = parsePromptRevision( + JSON.stringify(replay.response), + replay.subjectId, + Number(raw?.revision), + ); + authorizeRecord(prompt, context); + return prompt; +} + function object( value: unknown, field: string, diff --git a/src/server/search.ts b/src/server/search.ts index 511bab2..f541f33 100644 --- a/src/server/search.ts +++ b/src/server/search.ts @@ -1,196 +1,308 @@ -import type { - AuthorizedSearchServiceV1, - ExternalSearchDomainMutationCoordinatorV1, - ExternalSearchLoadedSourceV1, - ExternalSearchProducerCallbacksV1, - ExternalSearchProducerDescriptorV1, - ExternalSearchProducerGuardV1, - SearchMaterializationAdminGuardV1, - SearchProjectionFieldV1, - SearchProjectionSegmentV1, - SearchRequest, - SearchPage, - SemanticSearchAdminV1, - TaprootHostWriteCapability, - TaprootWriteOptions, - UnifiedSearchFiltersV1, - UnifiedSearchReferenceV1, -} from '@gnolith/taproot'; +import type { TaprootHostWriteCapability } from '@gnolith/taproot'; +import { + canonicalSearchHashV1, + createAuthorizedSearchServiceV1, + createSearchMaterializationAdminGuardV1, + createSemanticSearchAdminV1, + registerExternalProducerKindV1, + registerExternalProducerV1, + type ExternalProducer, + type ExternalProjectedSearchDocumentV1, + type ExternalSearchKind, + type ProducerSource, + type SearchMaterializationAdminGuardV1, + type SearchRequest, + type SearchResultV1, + type SemanticSearchAdminV1, +} from '@gnolith/taproot/search'; import type { AuthorizationContext } from '../protocol/authorization.js'; import { WorkshopError } from '../protocol/errors.js'; -import type { D1DatabaseLike, D1ResultLike } from './database.js'; import { + authorizeRecord, normalizeVisibilityScope, - serializeVisibilityScope, type VisibilityScopeV1, } from './authorization.js'; +import { + createWorkshopCanonicalOwnerAdapterV1, + type WorkshopSearchDomainV1, +} from './canonical-producer.js'; +import type { D1DatabaseLike } from './database.js'; export type WorkshopSearchSourceKind = 'task' | 'memory' | 'prompt'; -export interface WorkshopSearchDomainV1 { - readonly coordinator: ExternalSearchDomainMutationCoordinatorV1; - readonly producer: ExternalSearchProducerGuardV1; - readonly descriptor: Readonly; -} - -interface ExternalAuthorization { - version: 1; - sourceId: string; - sourceRevision: string; - sourcePolicyRevision: number; - workspaceId: string | null; - ownerPrincipalId: string; - visibility: VisibilityScopeV1; -} - -export interface WorkshopCanonicalSearchMutationV1 { - context: AuthorizationContext; - eventId: string; - sourceId: string; - operation: 'upsert' | 'delete'; - changeClass: string; - sourceRevision: number; - sourcePolicyRevision: number; - predecessor: { eventId: string; sequence: number } | null; - canonicalPostState: unknown; - statements: readonly { sql: string; values: readonly unknown[] }[]; -} - -export interface WorkshopCanonicalSearchMutationReceiptV1 { - eventId: string; - authorizationRevision: number; - searchGeneration: number; - results: readonly D1ResultLike[]; +export interface WorkshopSearchIntegrationDomainV1 extends WorkshopSearchDomainV1 { + /** Compatibility spelling for the pre-0.5 Workshop test/host surface. */ + readonly producer: { + adoptLegacyPage( + context: AuthorizationContext, + input: { cursor?: string | null; limit: number }, + ): Promise<{ + enumerated: number; + adopted: number; + cursor: string | null; + complete: boolean; + }>; + }; } -export async function commitWorkshopCanonicalSearchMutationV1( - domain: WorkshopSearchDomainV1, - input: WorkshopCanonicalSearchMutationV1, -): Promise { - const sealed = await domain.coordinator.sealCanonicalMutation({ - ...input, - context: input.context, - sourceRevision: String(input.sourceRevision), - }); - const receipt = await domain.producer.commitCanonicalMutation(sealed); - return receipt; +export type WorkshopSearchRequestV1 = Omit & { + kinds?: readonly ( + | 'statement' + | 'item' + | 'task' + | 'memory' + | 'prompt' + | 'resource' + | 'annotation' + )[]; +}; + +export type WorkshopSearchKindV1 = + | 'statement' + | 'item' + | 'task' + | 'memory' + | 'prompt' + | 'resource' + | 'annotation'; + +export type WorkshopSearchResultV1 = Omit & { + kind: WorkshopSearchKindV1; +}; + +export interface WorkshopSearchPageV1 { + results: WorkshopSearchResultV1[]; + cursor?: string; +} + +export interface WorkshopAuthorizedSearchServiceV1 { + search( + request: WorkshopSearchRequestV1, + context: AuthorizationContext, + ): Promise; + hydrate( + result: WorkshopSearchResultV1, + context: AuthorizationContext, + ): Promise; } export interface WorkshopSearchIntegrationV1 { - readonly task: WorkshopSearchDomainV1; - readonly memory: WorkshopSearchDomainV1; - readonly prompt: WorkshopSearchDomainV1; - readonly service: AuthorizedSearchServiceV1; + readonly task: WorkshopSearchIntegrationDomainV1; + readonly memory: WorkshopSearchIntegrationDomainV1; + readonly prompt: WorkshopSearchIntegrationDomainV1; + readonly service: WorkshopAuthorizedSearchServiceV1; readonly materialization: SearchMaterializationAdminGuardV1; readonly semantic: SemanticSearchAdminV1; search( - request: SearchRequest, + request: WorkshopSearchRequestV1, context: AuthorizationContext, - ): Promise; + ): Promise; } export interface CreateWorkshopSearchIntegrationOptionsV1 { db: D1DatabaseLike; - taproot: TaprootWriteOptions; + taproot: { baseIri: string; clock?: () => Date }; hostCapability: TaprootHostWriteCapability; installationId: string; registrationContext: AuthorizationContext; clock?: () => Date; createId?: () => string; + semantic?: { + fetch?: typeof fetch; + resolveCredential?: ( + selector: string, + ) => string | Promise | undefined; + }; } -const CONTRACT_VERSION = 'taproot-external-search-producer-v1'; -const AUTHORIZATION_VERSION = 'workshop-cnf-policy-v1'; +const SEARCH_NAMESPACE = 'workshop'; export async function createWorkshopSearchIntegrationV1( options: CreateWorkshopSearchIntegrationOptionsV1, ): Promise { - const { - canonicalSearchHashV1, - createAuthorizedSearchServiceV1, - createExternalSearchDomainMutationCoordinatorV1, - createExternalSearchDomainPolicyAuthorityV1, - createExternalSearchProducerGuardV1, - createSearchMaterializationAdminGuardV1, - createSemanticSearchAdminV1, - } = await import('@gnolith/taproot'); - const db = options.db; - const materialization = await createSearchMaterializationAdminGuardV1( - db, - options.taproot, - options.hostCapability, - ); - await materialization.initialize(options.registrationContext); - const domains: Array< - readonly [WorkshopSearchSourceKind, WorkshopSearchDomainV1] - > = []; + const domains = new Map< + WorkshopSearchSourceKind, + WorkshopSearchIntegrationDomainV1 + >(); + const domainKinds = new Map(); for (const sourceKind of ['task', 'memory', 'prompt'] as const) { - const binding = { - domain: 'workshop', - sourceKind, - capability: capabilityFor(sourceKind), - changeClasses: changeClassesFor(sourceKind), - } as const; - const coordinator = await createExternalSearchDomainMutationCoordinatorV1( - db, - options.taproot, - options.hostCapability, - binding, - ); - const authority = await createExternalSearchDomainPolicyAuthorityV1( - db, - options.taproot, + const controller = createWorkshopCanonicalOwnerAdapterV1(options.db); + const registeredKind = await registerExternalProducerKindV1( + options.db, + { baseIri: options.taproot.baseIri }, options.hostCapability, - binding, + { + installationId: options.installationId, + namespace: SEARCH_NAMESPACE, + localName: sourceKind, + ownerDomain: 'workshop', + adapter: controller.adapter, + }, ); - const descriptor: ExternalSearchProducerDescriptorV1 = { - version: 1, + const fingerprint = await canonicalSearchHashV1({ + package: '@gnolith/workshop', + version: '0.5.0', sourceKind, - owningDomain: 'workshop', - producerFingerprint: await canonicalSearchHashV1({ - package: '@gnolith/workshop', - sourceKind, - contractVersion: CONTRACT_VERSION, - projectionVersion: `workshop-${sourceKind}-projection-v1`, - authorizationContractVersion: AUTHORIZATION_VERSION, - }), - contractVersion: CONTRACT_VERSION, projectionVersion: `workshop-${sourceKind}-projection-v1`, - authorizationContractVersion: AUTHORIZATION_VERSION, + }); + const producer: ExternalProducer = { + descriptor: { + registration: registeredKind.registration, + implementationVersion: '0.5.0', + fingerprint, + projectionVersion: `workshop-${sourceKind}-projection-v1`, + changeClasses: changeClassesFor(sourceKind), + }, + async enumerate(cursor, limit) { + const page = await enumerate( + options.db, + options.installationId, + sourceKind, + cursor, + limit, + ); + return { ids: page.sourceIds, cursor: page.nextCursor }; + }, + async loadExact(sourceId, sourceRevision) { + const source = await load( + options.db, + options.installationId, + sourceKind, + sourceId, + sourceRevision ?? null, + ); + if (!source) + throw new WorkshopError( + 'not_found', + 'Canonical source not found', + 404, + ); + return source; + }, + project(source) { + return Promise.resolve(project(sourceKind, source)); + }, + async authorize(reference, context) { + const source = await load( + options.db, + options.installationId, + sourceKind, + reference.sourceId, + reference.sourceRevision, + ); + if (!source) return false; + try { + authorizeSource(source, context); + return true; + } catch { + return false; + } + }, + async hydrate(reference, context) { + const source = await load( + options.db, + options.installationId, + sourceKind, + reference.sourceId, + reference.sourceRevision, + ); + if (!source) + throw new WorkshopError( + 'not_found', + 'Canonical source not found', + 404, + ); + authorizeSource(source, context); + return source.canonical; + }, }; - const callbacks = callbacksFor( + const registered = await registerExternalProducerV1( options.db, - options.installationId, - sourceKind, - ); - const producer = await createExternalSearchProducerGuardV1( - db, - options.taproot, + { + baseIri: options.taproot.baseIri, + ...(options.clock ? { clock: options.clock } : {}), + }, options.hostCapability, - descriptor, - callbacks, - authority, - coordinator, + producer, ); - domains.push([sourceKind, { coordinator, producer, descriptor }]); + const domain = controller.bind(registered); + domainKinds.set(sourceKind, registered.kind); + domains.set(sourceKind, { + ...domain, + producer: { + async adoptLegacyPage(context, input) { + const receipt = await domain.adopt(context, input); + return { ...receipt, complete: receipt.cursor === null }; + }, + }, + }); } - const byKind = Object.fromEntries(domains) as Record< - WorkshopSearchSourceKind, - WorkshopSearchDomainV1 - >; - const semantic = createSemanticSearchAdminV1(db, { + + const semantic = createSemanticSearchAdminV1(options.db, { installationId: options.installationId, ...(options.clock ? { clock: options.clock } : {}), ...(options.createId ? { createId: options.createId } : {}), + ...(options.semantic?.fetch ? { fetch: options.semantic.fetch } : {}), + ...(options.semantic?.resolveCredential + ? { resolveCredential: options.semantic.resolveCredential } + : {}), }); - const service = createAuthorizedSearchServiceV1(db, { + const rawService = createAuthorizedSearchServiceV1(options.db, { installationId: options.installationId, semantic, }); + const materialization = await createSearchMaterializationAdminGuardV1( + options.db, + { + baseIri: options.taproot.baseIri, + ...(options.clock ? { clock: options.clock } : {}), + }, + options.hostCapability, + ); + await materialization.initialize(options.registrationContext); + const toTaprootRequest = ({ + kinds, + ...request + }: WorkshopSearchRequestV1): SearchRequest => ({ + ...request, + ...(kinds + ? { + kinds: kinds.map((kind) => + kind === 'task' || kind === 'memory' || kind === 'prompt' + ? requiredDomainKind(domainKinds, kind) + : kind, + ), + } + : {}), + }); + const service: WorkshopAuthorizedSearchServiceV1 = { + async search(request, context) { + const page = await rawService.search(toTaprootRequest(request), context); + return { + ...page, + results: page.results.map((result) => ({ + ...result, + kind: + typeof result.kind === 'string' + ? result.kind + : (result.kind.localName as WorkshopSearchKindV1), + })), + }; + }, + hydrate(result, context) { + const kind = + result.kind === 'task' || + result.kind === 'memory' || + result.kind === 'prompt' + ? requiredDomainKind(domainKinds, result.kind) + : result.kind; + return rawService.hydrate({ ...result, kind } as SearchResultV1, context); + }, + }; return { - task: byKind.task, - memory: byKind.memory, - prompt: byKind.prompt, + task: requiredDomain(domains, 'task'), + memory: requiredDomain(domains, 'memory'), + prompt: requiredDomain(domains, 'prompt'), service, semantic, materialization, @@ -198,47 +310,6 @@ export async function createWorkshopSearchIntegrationV1( }; } -function callbacksFor( - db: D1DatabaseLike, - installationId: string, - kind: WorkshopSearchSourceKind, -): ExternalSearchProducerCallbacksV1 { - return { - enumerateLegacyCurrent: ({ cursor, limit }) => - enumerate(db, installationId, kind, cursor, limit), - loadCurrent: ({ sourceId, expectedSourceRevision }) => - load(db, installationId, kind, sourceId, expectedSourceRevision), - projectCurrent: ({ source, authorization }) => - project(kind, source, authorization), - authorizeCurrentReference: async (authority, input) => { - assertAuthority(authority); - const source = await load( - db, - installationId, - kind, - input.sourceId, - input.sourceRevision, - ); - if (!source || source.sourcePolicyRevision !== input.sourcePolicyRevision) - throw denied(); - return authorizationOf(source); - }, - hydrateCurrentReference: async (authority, input) => { - assertAuthority(authority); - const source = await load( - db, - installationId, - kind, - input.sourceId, - input.sourceRevision, - ); - if (!source || source.sourcePolicyRevision !== input.sourcePolicyRevision) - throw denied(); - return source.canonical; - }, - }; -} - async function enumerate( db: D1DatabaseLike, installationId: string, @@ -246,19 +317,22 @@ async function enumerate( cursor: string | null, limit: number, ): Promise<{ sourceIds: readonly string[]; nextCursor: string | null }> { - if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw denied(); - const after = cursor ?? ''; + if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) + throw new WorkshopError( + 'validation_failed', + 'Search page limit is invalid', + ); const table = tableFor(kind); const id = idColumnFor(kind); - const deleted = kind === 'task' ? '1 = 1' : 'deleted_at IS NULL'; + const live = kind === 'task' ? '1 = 1' : 'deleted_at IS NULL'; const rows = await db .prepare( `SELECT ${id} AS id FROM ${table} - WHERE installation_id = ? AND ${id} > ? AND ${deleted} + WHERE installation_id = ? AND ${id} > ? AND ${live} AND visibility_scope IS NOT NULL AND authorization_revision IS NOT NULL ORDER BY ${id} ASC LIMIT ?`, ) - .bind(installationId, after, limit + 1) + .bind(installationId, cursor ?? '', limit + 1) .all<{ id: string }>(); const visible = rows.results.slice(0, limit); return { @@ -273,261 +347,140 @@ async function load( installationId: string, kind: WorkshopSearchSourceKind, sourceId: string, - expectedSourceRevision: string | null, -): Promise { - if (typeof sourceId !== 'string' || sourceId.length === 0) throw denied(); + expectedRevision: string | null, +): Promise { const table = tableFor(kind); const id = idColumnFor(kind); - const deleted = kind === 'task' ? '1 = 1' : 'deleted_at IS NULL'; + const live = kind === 'task' ? '1 = 1' : 'deleted_at IS NULL'; const row = await db .prepare( `SELECT * FROM ${table} - WHERE installation_id = ? AND ${id} = ? AND ${deleted}`, + WHERE installation_id = ? AND ${id} = ? AND ${live}`, ) .bind(installationId, sourceId) .first>(); if (!row) return null; - const revision = integer(row.revision, 'revision'); - if ( - expectedSourceRevision !== null && - expectedSourceRevision !== String(revision) - ) + const revision = integer(row.revision); + if (expectedRevision !== null && expectedRevision !== String(revision)) return null; - const canonical = canonicalOf(kind, row); return { sourceId, sourceRevision: String(revision), - sourcePolicyRevision: integer(row.policy_revision, 'policy_revision'), - canonical, + sourcePolicyRevision: integer(row.policy_revision), + canonical: canonicalRecord(kind, row), }; } -function authorizationOf( - source: ExternalSearchLoadedSourceV1, -): ExternalAuthorization { +function project( + kind: WorkshopSearchSourceKind, + source: ProducerSource, +): ExternalProjectedSearchDocumentV1 { const canonical = record(source.canonical); + const fields = + kind === 'task' + ? ['title', 'objective', 'description', 'prompt', 'result'] + : kind === 'memory' + ? ['title', 'description', 'content'] + : ['name', 'title', 'promptText', 'scope', 'role']; + const text = fields + .map((field) => canonical[field]) + .filter((value): value is string => typeof value === 'string' && !!value) + .join(' ') + // Taproot external projections are tokens: authored line breaks and other + // control characters must not cross the producer boundary. + .replace(/[\u0000-\u001f\u007f]+/gu, ' ') + .replace(/\s+/gu, ' ') + .trim(); + if (!text) + throw new WorkshopError('validation_failed', 'Source is not searchable'); return { - version: 1, - sourceId: source.sourceId, - sourceRevision: source.sourceRevision, - sourcePolicyRevision: source.sourcePolicyRevision, - workspaceId: nullableString(canonical.workspaceId), - ownerPrincipalId: string(canonical.ownerPrincipalId), - visibility: normalizeVisibilityScope( - canonical.visibility as VisibilityScopeV1, - ), + text, + ...(typeof canonical.title === 'string' + ? { title: canonical.title } + : typeof canonical.name === 'string' + ? { title: canonical.name } + : {}), + ...(typeof canonical.language === 'string' + ? { language: canonical.language } + : {}), }; } -function project( - kind: WorkshopSearchSourceKind, - source: ExternalSearchLoadedSourceV1, - authorization: ExternalAuthorization, -): Promise<{ - version: 1; - documents: Array<{ - documentSlot: string; - canonicalReference: UnifiedSearchReferenceV1; - filterMetadata: UnifiedSearchFiltersV1; - text: string; - segments: SearchProjectionSegmentV1[]; - }>; - replaceAll: true; - removeDocumentSlots: string[]; -}> { +function authorizeSource( + source: ProducerSource, + context: AuthorizationContext, +): void { const canonical = record(source.canonical); - const language = optionalString(canonical.language) ?? 'en'; - const fields: Array<[SearchProjectionFieldV1, string]> = []; - let filters: UnifiedSearchFiltersV1 = emptyFilters(language); - let reference: UnifiedSearchReferenceV1; - if (kind === 'task') { - reference = { kind, taskId: source.sourceId }; - push(fields, 'title', canonical.title); - push(fields, 'description', canonical.objective); - push(fields, 'description', canonical.description); - push(fields, 'prompt', canonical.prompt); - push(fields, 'description', stringify(canonical.constraints)); - push(fields, 'description', stringify(canonical.acceptanceCriteria)); - push(fields, 'description', stringify(canonical.relationships)); - const status = canonical.archivedAt - ? 'archived' - : canonical.completedAt - ? 'completed' - : canonical.claimed === true - ? 'claimed' - : 'unclaimed'; - push(fields, 'status', status); - push(fields, 'content', canonical.assignedPrincipalId); - push(fields, 'result', canonical.outcome); - filters = { - ...filters, - byKind: { task: { statuses: [status] } }, - }; - } else if (kind === 'memory') { - reference = { kind, memoryId: source.sourceId }; - push(fields, 'title', canonical.title); - push(fields, 'description', canonical.description); - push(fields, 'content', canonical.content); - push(fields, 'content', stringify(canonical.applicability)); - push(fields, 'content', stringify(canonical.provenance)); - } else { - reference = { kind, promptId: source.sourceId }; - push(fields, 'title', canonical.name); - push(fields, 'title', canonical.title); - push(fields, 'prompt', canonical.promptText); - push(fields, 'content', canonical.scope); - push(fields, 'content', canonical.role); - push(fields, 'content', stringify(canonical.variables)); - push(fields, 'status', canonical.active === true ? 'active' : 'inactive'); - push(fields, 'content', String(canonical.priority)); - push(fields, 'content', String(canonical.order)); - } - const { text, segments } = combine(source.sourceId, language, fields); - if (text.length === 0) throw denied(); - if ( - authorization.sourceId !== source.sourceId || - authorization.sourceRevision !== source.sourceRevision || - authorization.sourcePolicyRevision !== source.sourcePolicyRevision - ) - throw denied(); - return Promise.resolve({ - version: 1 as const, - documents: [ - { - documentSlot: 'canonical', - canonicalReference: reference, - filterMetadata: filters, - text, - segments, - }, - ], - replaceAll: true as const, - removeDocumentSlots: [], - }); + authorizeRecord( + { + installationId: string(canonical.installationId), + visibility: normalizeVisibilityScope( + canonical.visibility as VisibilityScopeV1, + ), + authorizationRevision: integer(canonical.authorizationRevision), + }, + context, + ); +} + +function requiredDomainKind( + domains: Map, + kind: WorkshopSearchSourceKind, +): ExternalSearchKind { + const domain = requiredDomain(domains, kind); + return domain; } -function canonicalOf( +function canonicalRecord( kind: WorkshopSearchSourceKind, row: Record, -) { - const shared = { - id: string(row[idColumnFor(kind)]), - revision: integer(row.revision, 'revision'), - policyRevision: integer(row.policy_revision, 'policy_revision'), - language: string(row.language), - attribution: json(row.attribution_json), - installationId: string(row.installation_id), - ownerPrincipalId: string(row.owner_principal_id), - workspaceId: string(row.workspace_id), - visibility: visibility(row.visibility_scope), - authorizationRevision: integer( - row.authorization_revision, - 'authorization_revision', - ), - createdAt: string(row.created_at), - updatedAt: string(row.updated_at), +): Record { + const common = { + installationId: row.installation_id, + ownerPrincipalId: row.owner_principal_id, + workspaceId: row.workspace_id, + visibility: json(row.visibility_scope), + authorizationRevision: row.authorization_revision, + revision: row.revision, + policyRevision: row.policy_revision, + language: row.language, }; if (kind === 'task') return { - ...shared, - title: string(row.title), - ...(optionalString(row.objective) - ? { objective: optionalString(row.objective) } - : {}), - description: string(row.description), - constraints: json(row.constraints_json), - acceptanceCriteria: json(row.acceptance_criteria_json), - relationships: json(row.relationships_json), - ...(optionalString(row.role) ? { role: optionalString(row.role) } : {}), - prompt: string(row.prompt), - claimed: integer(row.claimed, 'claimed') === 1, - ...(optionalString(row.claimed_at) - ? { claimedAt: normalizeDate(string(row.claimed_at)) } - : {}), - ...(optionalString(row.completed_at) - ? { completedAt: normalizeDate(string(row.completed_at)) } - : {}), - ...(optionalString(row.archived_at) - ? { archivedAt: normalizeDate(string(row.archived_at)) } - : {}), - ...(optionalString(row.assigned_principal_id) - ? { assignedPrincipalId: optionalString(row.assigned_principal_id) } - : {}), - ...(optionalString(row.result) - ? { result: optionalString(row.result) } - : {}), - ...(optionalString(row.outcome_kind) - ? { outcomeKind: optionalString(row.outcome_kind) } - : {}), - memorySlugs: json(row.memory_slugs), - contextQueries: json(row.context_queries), + ...common, + id: row.id, + title: row.title, + objective: row.objective, + description: row.description, + prompt: row.prompt, + result: row.result, }; - if (kind === 'memory') { - const { id, ...memoryShared } = shared; + if (kind === 'memory') return { - ...memoryShared, - slug: id, - title: string(row.title), - description: string(row.description), - content: string(row.content), - applicability: json(row.applicability_json), - provenance: json(row.provenance_json), + ...common, + slug: row.slug, + title: row.title, + description: row.description, + content: row.content, }; - } - return { - ...shared, - name: string(row.name), - title: string(row.title), - promptText: string(row.prompt_text), - ...(optionalString(row.scope) ? { scope: optionalString(row.scope) } : {}), - ...(optionalString(row.role) ? { role: optionalString(row.role) } : {}), - variables: json(row.variables_json), - active: integer(row.active, 'active') === 1, - priority: integer(row.priority, 'priority'), - order: integer(row.sort_order, 'sort_order'), - ...(optionalString(row.deactivated_at) - ? { deactivatedAt: optionalString(row.deactivated_at) } - : {}), - }; -} - -function combine( - sourceId: string, - language: string, - fields: readonly [SearchProjectionFieldV1, string][], -): { text: string; segments: SearchProjectionSegmentV1[] } { - let text = ''; - const segments: SearchProjectionSegmentV1[] = []; - for (const [field, value] of fields) { - if (text) text += '\n'; - const start = text.length; - text += value; - segments.push({ - field, - sourceId, - language, - text: value, - documentStart: start, - documentEnd: text.length, - }); - } - return { text, segments }; -} - -function emptyFilters(language: string): UnifiedSearchFiltersV1 { return { - languages: [language], - sourceRevisions: [], - byKind: {}, + ...common, + id: row.id, + name: row.name, + title: row.title, + promptText: row.prompt_text, + scope: row.scope, + role: row.role, }; } -function push( - fields: Array<[SearchProjectionFieldV1, string]>, - field: SearchProjectionFieldV1, - value: unknown, -): void { - if (typeof value === 'string' && value.trim()) fields.push([field, value]); +function requiredDomain( + domains: Map, + kind: WorkshopSearchSourceKind, +): T { + const domain = domains.get(kind); + if (!domain) throw new Error(`Missing Workshop ${kind} search domain`); + return domain; } function tableFor(kind: WorkshopSearchSourceKind): string { @@ -539,15 +492,7 @@ function tableFor(kind: WorkshopSearchSourceKind): string { } function idColumnFor(kind: WorkshopSearchSourceKind): string { - return kind === 'task' ? 'id' : kind === 'memory' ? 'slug' : 'id'; -} - -function capabilityFor(kind: WorkshopSearchSourceKind): string { - return kind === 'task' - ? 'task-write' - : kind === 'memory' - ? 'memory-write' - : 'prompt-write'; + return kind === 'memory' ? 'slug' : 'id'; } function changeClassesFor(kind: WorkshopSearchSourceKind): readonly string[] { @@ -561,63 +506,31 @@ function changeClassesFor(kind: WorkshopSearchSourceKind): readonly string[] { ]; } -function normalizeDate(value: string): string { - return new Date( - value.includes('T') ? value : `${value.replace(' ', 'T')}Z`, - ).toISOString(); +function integer(value: unknown): number { + const number = Number(value); + if (!Number.isSafeInteger(number)) + throw new WorkshopError('internal_error', 'Invalid canonical integer'); + return number; } -function visibility(value: unknown): VisibilityScopeV1 { - const parsed = json(value); - const normalized = normalizeVisibilityScope(parsed as VisibilityScopeV1); - if (serializeVisibilityScope(normalized) !== String(value)) throw denied(); - return normalized; +function string(value: unknown): string { + if (typeof value !== 'string' || !value) + throw new WorkshopError('internal_error', 'Invalid canonical string'); + return value; } function json(value: unknown): unknown { - if (typeof value !== 'string') throw denied(); + if (typeof value !== 'string') + throw new WorkshopError('internal_error', 'Invalid canonical JSON'); try { return JSON.parse(value); } catch { - throw denied(); + throw new WorkshopError('internal_error', 'Invalid canonical JSON'); } } function record(value: unknown): Record { - if (typeof value !== 'object' || value === null || Array.isArray(value)) - throw denied(); + if (!value || typeof value !== 'object' || Array.isArray(value)) + throw new WorkshopError('internal_error', 'Invalid canonical record'); return value as Record; } - -function string(value: unknown): string { - if (typeof value !== 'string' || !value) throw denied(); - return value; -} - -function optionalString(value: unknown): string | undefined { - return typeof value === 'string' && value ? value : undefined; -} - -function nullableString(value: unknown): string | null { - return value === null || value === undefined ? null : string(value); -} - -function integer(value: unknown, field: string): number { - const number = Number(value); - if (!Number.isSafeInteger(number)) - throw new WorkshopError('internal_error', `Invalid canonical ${field}`); - return number; -} - -function stringify(value: unknown): string { - return typeof value === 'string' ? value : JSON.stringify(value); -} - -function assertAuthority(value: { readonly kind: string }): void { - if (value.kind !== 'taproot-external-search-domain-policy-authority-v1') - throw denied(); -} - -function denied(): WorkshopError { - return new WorkshopError('forbidden', 'Authorization denied'); -} diff --git a/src/server/service.ts b/src/server/service.ts new file mode 100644 index 0000000..922a4f2 --- /dev/null +++ b/src/server/service.ts @@ -0,0 +1,546 @@ +import { createServer, type Server } from 'node:http'; +import { Readable } from 'node:stream'; +import { + OPERATION_ROUTE_PREFIX, + operationCatalog, + type OperationName, +} from '../generated/operations.js'; +import type { + OperationRuntime, + WorkshopStatusSnapshot, +} from '../operations.js'; +import { WorkshopError, normalizeWorkshopError } from '../protocol/errors.js'; +import type { AuthenticationVerifier } from './auth.js'; +import type { VerifiedWaystoneAssets } from './assets.js'; +import type { ListenerConfig } from './config.js'; +import { validateListenerConfig } from './config.js'; +import type { BoundedTriggerController } from './lifecycle.js'; +import { createGnolithMcpServer } from '../mcp/server.js'; +import { + assertVerifiedWorkshopProducerBinding, + type VerifiedWorkshopProducerBinding, + type WorkshopProducerBindingExpectation, +} from '../producers.js'; + +export interface CloseableDependency { + close(): void | Promise; +} + +export interface WorkshopHttpServiceOptions { + version: string; + listener: ListenerConfig; + authentication: AuthenticationVerifier; + operations: OperationRuntime; + producerBinding: { + binding: VerifiedWorkshopProducerBinding; + expectation: WorkshopProducerBindingExpectation; + }; + preflight(signal: AbortSignal): Promise; + corsOrigins?: readonly string[]; + assets?: VerifiedWaystoneAssets; + trigger?: BoundedTriggerController; + dependencies?: readonly CloseableDependency[]; + shutdownTimeoutMs?: number; + logger?: { + info(event: Readonly>): void; + error(event: Readonly>): void; + }; +} + +export interface WorkshopHttpService { + readonly listener: ListenerConfig; + readonly fetch: (request: Request) => Promise; + start(): Promise<{ host: string; port: number }>; + stop(): Promise; +} + +export function createWorkshopHttpService( + options: WorkshopHttpServiceOptions, +): WorkshopHttpService { + const listener = validateListenerConfig(options.listener); + assertVerifiedWorkshopProducerBinding( + options.producerBinding.binding, + options.producerBinding.expectation, + ); + const origins = new Set(options.corsOrigins ?? []); + const mcp = createGnolithMcpServer({ + version: options.version, + operations: options.operations, + authenticate: (request) => options.authentication.authenticate(request), + allowedOrigins: [...origins], + challenge: options.authentication.challenge, + }); + let server: Server | undefined; + let started = false; + let draining = false; + let active = 0; + const drained = new Set<() => void>(); + const fetch = async (request: Request): Promise => { + if (draining && request.method !== 'GET') + return errorResponse( + new WorkshopError('dependency_unavailable', 'Service is draining', 503), + request, + origins, + ); + active += 1; + try { + const url = new URL(request.url); + if (url.pathname === '/health/live' && request.method === 'GET') + return Response.json( + { status: 'live' }, + { + headers: { + 'cache-control': 'no-store', + 'x-content-type-options': 'nosniff', + }, + }, + ); + if ( + url.pathname === '/.well-known/oauth-protected-resource/mcp' && + request.method === 'GET' + ) { + const metadata = options.authentication.protectedResourceMetadata(); + return metadata + ? Response.json(metadata, { + headers: { 'cache-control': 'public, max-age=300' }, + }) + : new Response('Not found', { status: 404 }); + } + if (url.pathname === '/mcp') return mcp.handle(request); + if ( + url.pathname.startsWith(`${OPERATION_ROUTE_PREFIX}/`) && + request.method === 'OPTIONS' + ) { + const originFailure = validateOrigin(request, origins); + if (originFailure) return originFailure; + const headers = responseHeaders(request, origins); + headers.set('access-control-allow-methods', 'POST, OPTIONS'); + headers.set( + 'access-control-allow-headers', + 'authorization, content-type, x-request-id', + ); + headers.set('access-control-max-age', '600'); + return new Response(null, { status: 204, headers }); + } + if (url.pathname === '/health/ready' && request.method === 'GET') { + try { + const principal = await options.authentication.authenticate(request); + const value = await options.operations.call( + 'gnolith_status', + {}, + { + principal, + requestId: requestId(request), + signal: request.signal, + }, + ); + const ready = readinessFromStatus(value); + return Response.json(value, { + status: ready ? 200 : 503, + headers: responseHeaders(request, origins), + }); + } catch (error) { + return errorResponse(error, request, origins); + } + } + if ( + url.pathname.startsWith(`${OPERATION_ROUTE_PREFIX}/`) && + request.method === 'POST' + ) + return operationResponse(request, options, origins); + const assetResponse = await options.assets?.serve(url.pathname); + if (assetResponse) return assetResponse; + return new Response('Not found', { status: 404 }); + } finally { + active -= 1; + if (active === 0) { + for (const resolve of drained) resolve(); + drained.clear(); + } + } + }; + return { + listener, + fetch, + async start() { + if (started) + throw new WorkshopError('conflict', 'Service is already started', 409); + const preflight = await options.preflight(new AbortController().signal); + if (!startupReady(preflight)) + throw new WorkshopError( + 'dependency_unavailable', + 'Required migrations, authorization, canonical data, or producers are not ready; run the explicit migration/bootstrap workflow', + 503, + ); + server = createServer((incoming, outgoing) => { + // IncomingMessage.url is the only request-target representation that + // still preserves encoded dot segments. Fence it synchronously in the + // Node listener before constructing a WHATWG URL or entering any + // health, MCP, operation, or asset router. + try { + rejectEncodedTraversalTarget(incoming.url ?? '/'); + } catch (error) { + writeNodeError(error, outgoing); + return; + } + void handleNodeExchange(incoming, outgoing, listener, fetch); + }); + server.requestTimeout = 35_000; + server.headersTimeout = 10_000; + server.keepAliveTimeout = 5_000; + await new Promise((resolve, reject) => { + server!.once('error', reject); + server!.listen(listener.port, listener.host, () => { + server!.off('error', reject); + resolve(); + }); + }); + started = true; + options.trigger?.start(); + const address = server.address(); + const port = + typeof address === 'object' && address ? address.port : listener.port; + options.logger?.info({ + event: 'service.started', + profile: listener.profile, + host: listener.host, + port, + }); + return { host: listener.host, port }; + }, + async stop() { + if (!started || !server) return; + draining = true; + const closingServer = server; + const closed = new Promise((resolve, reject) => { + closingServer.close((error) => + error && + (error as NodeJS.ErrnoException).code !== 'ERR_SERVER_NOT_RUNNING' + ? reject(error) + : resolve(), + ); + }); + server.closeIdleConnections(); + const timeoutMs = options.shutdownTimeoutMs ?? 30_000; + const abort = new AbortController(); + const timeout = setTimeout( + () => abort.abort(new Error('shutdown timeout')), + timeoutMs, + ); + try { + await Promise.all([ + waitForDrain(() => active, drained, abort.signal), + ...(options.trigger ? [options.trigger.stop(abort.signal)] : []), + ]); + for (const dependency of options.dependencies ?? []) + await dependency.close(); + await Promise.race([closed, aborted(abort.signal)]); + } finally { + clearTimeout(timeout); + started = false; + server = undefined; + options.logger?.info({ event: 'service.stopped' }); + } + }, + }; +} + +function aborted(signal: AbortSignal): Promise { + return new Promise((_, reject) => { + if (signal.aborted) { + reject(signal.reason); + return; + } + signal.addEventListener('abort', () => reject(signal.reason), { + once: true, + }); + }); +} + +async function operationResponse( + request: Request, + options: WorkshopHttpServiceOptions, + origins: ReadonlySet, +): Promise { + const originFailure = validateOrigin(request, origins); + if (originFailure) return originFailure; + try { + const principal = await options.authentication.authenticate(request); + const name = new URL(request.url).pathname.slice( + OPERATION_ROUTE_PREFIX.length + 1, + ); + if (!operationCatalog.some((operation) => operation.name === name)) + return errorResponse( + new WorkshopError('not_found', 'Operation not found', 404), + request, + origins, + ); + const input = await boundedJson(request, 1024 * 1024); + const value = await options.operations.call(name as OperationName, input, { + principal, + requestId: requestId(request), + signal: request.signal, + }); + return Response.json(value, { + headers: responseHeaders(request, origins), + }); + } catch (error) { + return errorResponse(error, request, origins); + } +} + +function startupReady(status: WorkshopStatusSnapshot): boolean { + return ( + status.migrations.ready && + status.compatibility.diamond && + status.compatibility.taproot && + status.canonicalReady && + status.authorizationReady && + status.producers.ready && + status.blobReady + ); +} + +function readinessFromStatus(value: unknown): boolean { + if (!value || typeof value !== 'object') return false; + const status = value as Record; + const migration = status.migrationReadiness as + Record | undefined; + const producers = status.producers as Record | undefined; + const semantic = status.semanticState as Record | undefined; + return ( + migration?.ready === true && + status.canonicalReady === true && + status.authorizationReady === true && + status.lexicalReady === true && + (semantic?.configured !== true || semantic.ready === true) && + producers?.ready === true && + status.blobReady === true + ); +} + +function responseHeaders( + request: Request, + origins: ReadonlySet, +): Headers { + const headers = new Headers({ + 'cache-control': 'no-store', + 'x-content-type-options': 'nosniff', + 'content-security-policy': "default-src 'none'", + }); + const origin = request.headers.get('origin'); + if ( + origin && + (origin === new URL(request.url).origin || origins.has(origin)) + ) { + headers.set('access-control-allow-origin', origin); + headers.set('vary', 'Origin'); + } + return headers; +} + +function validateOrigin( + request: Request, + origins: ReadonlySet, +): Response | null { + const origin = request.headers.get('origin'); + if (origin && origin !== new URL(request.url).origin && !origins.has(origin)) + return Response.json( + { error: { code: 'forbidden', message: 'Origin is not allowed' } }, + { status: 403 }, + ); + return null; +} + +function errorResponse( + error: unknown, + request: Request, + origins: ReadonlySet, +): Response { + const normalized = normalizeWorkshopError(error); + const message = + normalized.code === 'internal_error' + ? 'Workshop operation failed' + : normalized.message; + return Response.json( + { + error: { + code: normalized.code, + message, + requestId: requestId(request), + ...(normalized.details ? { details: normalized.details } : {}), + }, + }, + { + status: normalized.status, + headers: responseHeaders(request, origins), + }, + ); +} + +async function boundedJson( + request: Request, + maxBytes: number, +): Promise> { + if ( + request.headers.get('content-type')?.split(';', 1)[0] !== 'application/json' + ) + throw new WorkshopError( + 'bad_request', + 'Request requires application/json', + 415, + ); + const bytes = new Uint8Array(await request.arrayBuffer()); + if (bytes.byteLength > maxBytes) + throw new WorkshopError('limit_exceeded', 'Request body is too large', 413); + try { + const value = JSON.parse(new TextDecoder().decode(bytes)) as unknown; + if (!value || typeof value !== 'object' || Array.isArray(value)) + throw new Error('object required'); + return value as Record; + } catch { + throw new WorkshopError('bad_request', 'Invalid JSON object body', 400); + } +} + +function requestId(request: Request): string { + const supplied = request.headers.get('x-request-id'); + return supplied && + supplied.length <= 256 && + !/[\u0000-\u001f\u007f]/u.test(supplied) + ? supplied + : crypto.randomUUID(); +} + +async function waitForDrain( + active: () => number, + drained: Set<() => void>, + signal: AbortSignal, +): Promise { + if (active() === 0) return; + await new Promise((resolve, reject) => { + const completed = () => { + signal.removeEventListener('abort', cancelled); + resolve(); + }; + const cancelled = () => { + drained.delete(completed); + reject(signal.reason); + }; + drained.add(completed); + signal.addEventListener('abort', cancelled, { once: true }); + }); +} + +function nodeRequest( + incoming: import('node:http').IncomingMessage, + listener: ListenerConfig, +): Request { + const origin = `http://${formatHost(listener.host)}:${listener.port}`; + const headers = new Headers(); + for (const [name, value] of Object.entries(incoming.headers)) { + if (Array.isArray(value)) { + for (const entry of value) headers.append(name, entry); + } else if (value !== undefined) headers.set(name, value); + } + const method = incoming.method ?? 'GET'; + return new Request(new URL(incoming.url ?? '/', origin), { + method, + headers, + ...(method === 'GET' || method === 'HEAD' + ? {} + : { + body: Readable.toWeb(incoming), + duplex: 'half', + }), + } as RequestInit); +} + +function rejectEncodedTraversalTarget(rawTarget: string): void { + let path = rawTarget.split(/[?#]/u, 1)[0] ?? ''; + const absolute = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/]*(.*)$/u.exec(path); + if (absolute) path = absolute[1] || '/'; + let candidate = path; + for (let depth = 0; depth < 3; depth += 1) { + if (/%(?:2e|2f|5c)/iu.test(candidate)) throw encodedTraversal(); + if ( + candidate + .split(/[\\/]/u) + .some((segment) => segment === '.' || segment === '..') + ) + throw encodedTraversal(); + if (!/%[0-9a-f]{2}/iu.test(candidate)) break; + try { + const decoded = decodeURIComponent(candidate); + if (decoded === candidate) break; + candidate = decoded; + } catch { + throw encodedTraversal(); + } + } +} + +function encodedTraversal(): WorkshopError { + return new WorkshopError( + 'bad_request', + 'Encoded path traversal is not allowed', + 400, + ); +} + +async function writeNodeResponse( + response: Response, + outgoing: import('node:http').ServerResponse, +): Promise { + outgoing.statusCode = response.status; + for (const [name, value] of response.headers) outgoing.setHeader(name, value); + if (!response.body) { + outgoing.end(); + return; + } + await new Promise((resolve, reject) => { + const stream = Readable.fromWeb(response.body as never); + stream.once('error', reject); + outgoing.once('error', reject); + outgoing.once('finish', resolve); + stream.pipe(outgoing); + }); +} + +async function handleNodeExchange( + incoming: import('node:http').IncomingMessage, + outgoing: import('node:http').ServerResponse, + listener: ListenerConfig, + fetchHandler: (request: Request) => Promise, +): Promise { + try { + const response = await fetchHandler(nodeRequest(incoming, listener)); + await writeNodeResponse(response, outgoing); + } catch (error) { + writeNodeError(error, outgoing); + } +} + +function writeNodeError( + error: unknown, + outgoing: import('node:http').ServerResponse, +): void { + const normalized = normalizeWorkshopError(error); + outgoing.statusCode = normalized.status; + outgoing.setHeader('content-type', 'application/json'); + outgoing.end( + JSON.stringify({ + error: { + code: normalized.code, + message: + normalized.code === 'internal_error' + ? 'Workshop operation failed' + : normalized.message, + }, + }), + ); +} + +function formatHost(host: string): string { + return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host; +} diff --git a/src/server/tasks.ts b/src/server/tasks.ts index 0fac5a7..2a4b3a8 100644 --- a/src/server/tasks.ts +++ b/src/server/tasks.ts @@ -46,7 +46,7 @@ import type { ContextQueryValidator } from './sparql.js'; import { commitWorkshopCanonicalSearchMutationV1, type WorkshopSearchDomainV1, -} from './search.js'; +} from './canonical-producer.js'; import { contextQueries, isoDate, @@ -56,6 +56,12 @@ import { requiredText, validation, } from './validation.js'; +import { digestWorkshopOperationRequest } from './idempotency.js'; + +const LEASE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/u; +const LEASE_DIGEST_PREFIX = new TextEncoder().encode( + 'gnolith-task-lease-v1\u0000', +); interface TaskRow { id: string; @@ -90,6 +96,38 @@ interface TaskRow { authorization_revision: number | null; search_event_id: string | null; search_event_sequence: number | null; + lease_id: string | null; + lease_token_digest: string | null; + lease_owner_principal_id: string | null; + lease_expires_at: string | null; +} + +function base64url(bytes: Uint8Array): string { + return Buffer.from(bytes).toString('base64url'); +} + +function decodeLeaseToken(value: string): Uint8Array { + if (!LEASE_TOKEN_PATTERN.test(value)) + throw new WorkshopError('validation_failed', 'Lease token is invalid', 400); + const bytes = Uint8Array.from(Buffer.from(value, 'base64url')); + if (bytes.byteLength !== 32) { + bytes.fill(0); + throw new WorkshopError('validation_failed', 'Lease token is invalid', 400); + } + return bytes; +} + +async function digestLeaseToken(token: Uint8Array): Promise { + const input = new Uint8Array(LEASE_DIGEST_PREFIX.length + token.length); + input.set(LEASE_DIGEST_PREFIX); + input.set(token, LEASE_DIGEST_PREFIX.length); + try { + return Buffer.from(await crypto.subtle.digest('SHA-256', input)).toString( + 'hex', + ); + } finally { + input.fill(0); + } } const AUTHORIZED_ID_BATCH = 80; @@ -104,10 +142,25 @@ export interface TaskServiceOptions { search?: WorkshopSearchDomainV1; } +export interface TaskLeaseClaim { + task: Task; + leaseId: string; + leaseToken: string; + expiresAt: string; +} + +export interface TaskLeaseReleaseReceipt { + taskId: string; + released: true; + revision: number; + replayed: boolean; +} + export class TaskService { readonly #limits: WorkshopLimits; readonly #clock: () => Date; readonly #createId: () => string; + readonly #taskMutationTails = new Map>(); constructor( readonly db: D1DatabaseLike, @@ -290,7 +343,7 @@ export class TaskService { input: CreateTaskInput, authorization: AuthorizationContext, ): Promise { - const context = await this.#context(authorization, 'task-write'); + const context = await this.#context(authorization, 'task:write'); const workspaceId = requireActiveWorkspace(context); const scope = normalizeVisibilityScope( input.visibility ?? defaultVisibility(context), @@ -409,8 +462,13 @@ export class TaskService { id: string, input: UpdateTaskInput, authorization: AuthorizationContext, + lease?: { + tokenDigest: string; + principalId: string; + expiresAfter: string; + }, ): Promise { - const context = await this.#context(authorization, 'task-write'); + const context = await this.#context(authorization, 'task:write'); const current = await this.#getForMutation(id, context); if (current.completedAt || current.archivedAt) { throw new WorkshopError( @@ -562,6 +620,7 @@ export class TaskService { visibility_scope = ?, authorization_revision = ?, policy_revision = ?, revision = ?, updated_at = ?, search_event_id = ?, search_event_sequence = NULL WHERE id = ? AND ${expected.column} = ? AND completed_at IS NULL AND archived_at IS NULL + ${lease ? 'AND lease_token_digest = ? AND lease_owner_principal_id = ? AND lease_expires_at > ?' : ''} AND ${predicate.sql} RETURNING *`, values: [ @@ -586,6 +645,9 @@ export class TaskService { eventId, id, expected.value, + ...(lease + ? [lease.tokenDigest, lease.principalId, lease.expiresAfter] + : []), ...predicate.values, ], }, @@ -604,19 +666,64 @@ export class TaskService { }); } + async updateWithLease( + id: string, + input: UpdateTaskInput, + leaseTokenValue: unknown, + authorization: AuthorizationContext, + ): Promise { + const context = await this.#context(authorization, 'task:write'); + const leaseToken = requiredText(leaseTokenValue, 'leaseToken', 4096); + const tokenBytes = decodeLeaseToken(leaseToken); + const tokenDigest = await digestLeaseToken(tokenBytes); + tokenBytes.fill(0); + return this.update(id, input, context, { + tokenDigest, + principalId: context.principalId, + expiresAfter: this.#clock().toISOString(), + }); + } + async archive( id: string, options: { expectedRevision?: number; expectedUpdatedAt?: string; + leaseToken?: unknown; }, authorization: AuthorizationContext, ): Promise { - const context = await this.#context(authorization, 'task-write'); + const context = await this.#context(authorization, 'task:write'); const current = await this.#getForMutation(id, context); const predicate = visibilitySql('workshop_tasks', context); const expected = revisionPrecondition(options); const now = this.#clock().toISOString(); + let leaseTokenDigest: string | null = null; + if (options.leaseToken !== undefined) { + const leaseToken = requiredText(options.leaseToken, 'leaseToken', 4096); + const tokenBytes = decodeLeaseToken(leaseToken); + leaseTokenDigest = await digestLeaseToken(tokenBytes); + tokenBytes.fill(0); + } + if (leaseTokenDigest !== null && !context.capabilities.includes('admin')) { + const ownedLease = await this.db + .prepare( + `SELECT lease_id FROM workshop_tasks + WHERE installation_id = ? AND id = ? + AND lease_token_digest = ? AND lease_owner_principal_id = ? + AND lease_expires_at > ?`, + ) + .bind( + context.installationId, + id, + leaseTokenDigest, + context.principalId, + now, + ) + .first<{ lease_id: string }>(); + if (!ownedLease) + throw new WorkshopError('conflict', 'Task lease is not owned', 409); + } return this.#mutation('task.archive', context.principalId, id, async () => { const eventId = crypto.randomUUID(); const next: Task = { @@ -634,10 +741,20 @@ export class TaskService { await this.#predecessor(id, context), { sql: `UPDATE workshop_tasks SET archived_at = ?, claimed = 0, claimed_at = NULL, + lease_id = NULL, lease_token_digest = NULL, + lease_owner_principal_id = NULL, lease_expires_at = NULL, revision = ?, authorization_revision = ?, updated_at = ?, search_event_id = ?, search_event_sequence = NULL WHERE id = ? AND ${expected.column} = ? AND completed_at IS NULL AND archived_at IS NULL - AND (claimed = 0 OR ? = 1) AND ${predicate.sql} RETURNING *`, + AND ( + ? = 1 + OR (claimed = 0 AND ? IS NULL) + OR ( + lease_token_digest = ? AND lease_owner_principal_id = ? + AND lease_expires_at > ? + ) + ) + AND ${predicate.sql} RETURNING *`, values: [ next.archivedAt, next.revision, @@ -647,6 +764,10 @@ export class TaskService { id, expected.value, context.capabilities.includes('admin') ? 1 : 0, + leaseTokenDigest, + leaseTokenDigest, + context.principalId, + now, ...predicate.values, ], }, @@ -663,7 +784,7 @@ export class TaskService { } async claim(id: string, authorization: AuthorizationContext): Promise { - const context = await this.#context(authorization, 'task-write'); + const context = await this.#context(authorization, 'task:write'); const current = await this.#getForMutation(id, context); const predicate = visibilitySql('workshop_tasks', context); const now = this.#clock().toISOString(); @@ -711,14 +832,381 @@ export class TaskService { }); } + async claimLease( + idValue: unknown, + leaseSecondsValue: unknown, + authorization: AuthorizationContext, + ): Promise { + const id = requiredText(idValue, 'taskId', 256); + const leaseSeconds = + leaseSecondsValue === undefined ? 900 : Number(leaseSecondsValue); + if ( + !Number.isSafeInteger(leaseSeconds) || + leaseSeconds < 1 || + leaseSeconds > 86_400 + ) + throw new WorkshopError( + 'validation_failed', + 'leaseSeconds must be an integer between 1 and 86400', + 400, + ); + return this.#serializeTaskMutation(id, async () => { + const context = await this.#context(authorization, 'task:write'); + const predicate = visibilitySql('workshop_tasks', context); + const now = this.#clock(); + const claimedAt = now.toISOString(); + const expiresAt = new Date( + now.getTime() + leaseSeconds * 1000, + ).toISOString(); + const leaseId = this.#createId(); + const tokenBytes = crypto.getRandomValues(new Uint8Array(32)); + const leaseToken = base64url(tokenBytes); + const tokenDigest = await digestLeaseToken(tokenBytes); + tokenBytes.fill(0); + const priorLease = await this.db + .prepare( + `SELECT lease_id, lease_expires_at FROM workshop_tasks + WHERE id = ? AND installation_id = ?`, + ) + .bind(id, context.installationId) + .first<{ lease_id: string | null; lease_expires_at: string | null }>(); + const eventId = this.#createId(); + const current = await this.#getForMutation(id, context); + if ( + priorLease?.lease_id && + priorLease.lease_expires_at && + priorLease.lease_expires_at > claimedAt + ) + throw new WorkshopError( + 'conflict', + 'Task is unavailable or has an active lease', + 409, + ); + const next: Task = { + ...current, + claimed: true, + claimedAt: nextTimestamp(claimedAt, current.updatedAt), + revision: current.revision + 1, + updatedAt: nextTimestamp(claimedAt, current.updatedAt), + authorizationRevision: context.authorizationRevision, + }; + let row: TaskRow | null; + try { + row = await this.#commitCanonical( + context, + next, + 'task.state', + await this.#predecessor(id, context), + { + sql: `UPDATE workshop_tasks + SET claimed = 1, claimed_at = ?, lease_id = ?, + lease_token_digest = ?, lease_owner_principal_id = ?, + lease_expires_at = ?, revision = revision + 1, + authorization_revision = ?, updated_at = ?, + search_event_id = ?, search_event_sequence = NULL + WHERE id = ? AND revision = ? AND completed_at IS NULL AND archived_at IS NULL + AND (lease_id IS NULL OR lease_expires_at <= ?) + AND ${predicate.sql} + RETURNING *`, + values: [ + next.claimedAt, + leaseId, + tokenDigest, + context.principalId, + expiresAt, + context.authorizationRevision, + next.updatedAt, + eventId, + id, + current.revision, + claimedAt, + ...predicate.values, + ], + }, + eventId, + [ + { + sql: `INSERT INTO workshop_task_lease_events + (event_id, installation_id, task_id, lease_id, actor_principal_id, + event_kind, task_revision, lease_expires_at, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + values: [ + eventId, + context.installationId, + id, + leaseId, + context.principalId, + priorLease?.lease_id && + priorLease.lease_expires_at && + priorLease.lease_expires_at <= claimedAt + ? 'reclaimed_expired' + : 'claimed', + next.revision, + expiresAt, + claimedAt, + ], + }, + ], + ); + } catch (error) { + if ( + await this.#claimRaceLost( + id, + context.installationId, + current.revision, + claimedAt, + error, + ) + ) + throw new WorkshopError( + 'conflict', + 'Task is unavailable or has an active lease', + 409, + ); + throw error; + } + if (!row) + throw new WorkshopError( + 'conflict', + 'Task is unavailable or has an active lease', + 409, + ); + return { task: toTask(row), leaseId, leaseToken, expiresAt }; + }); + } + + async #claimRaceLost( + taskId: string, + installationId: string, + expectedRevision: number, + claimedAt: string, + cause: unknown, + ): Promise { + for (let attempt = 0; attempt < 8; attempt += 1) { + try { + const row = await this.db + .prepare( + `SELECT revision, lease_id, lease_expires_at + FROM workshop_tasks + WHERE id = ? AND installation_id = ?`, + ) + .bind(taskId, installationId) + .first<{ + revision: number; + lease_id: string | null; + lease_expires_at: string | null; + }>(); + if ( + row && + (row.revision !== expectedRevision || + (row.lease_id !== null && + row.lease_expires_at !== null && + row.lease_expires_at > claimedAt)) + ) + return true; + } catch { + // A concurrent native transaction may still hold the database lock. + } + await new Promise((resolve) => { + setTimeout(resolve, 5 * (attempt + 1)); + }); + } + return ( + cause instanceof Error && + /SQLITE_BUSY|database is locked|constraint failed|transaction conflict/iu.test( + cause.message, + ) + ); + } + + async releaseLease( + input: { + taskId: unknown; + leaseToken: unknown; + expectedRevision: unknown; + idempotencyKey: unknown; + }, + authorization: AuthorizationContext, + ): Promise { + const taskId = requiredText(input.taskId, 'taskId', 256); + const leaseToken = requiredText(input.leaseToken, 'leaseToken', 4096); + const idempotencyKey = requiredText( + input.idempotencyKey, + 'idempotencyKey', + 256, + ); + const expectedRevision = Number(input.expectedRevision); + if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 1) + throw new WorkshopError( + 'validation_failed', + 'expectedRevision is invalid', + 400, + ); + const context = await this.#context(authorization, 'task:write'); + const tokenBytes = decodeLeaseToken(leaseToken); + const tokenDigest = await digestLeaseToken(tokenBytes); + tokenBytes.fill(0); + const requestDigest = await digestWorkshopOperationRequest({ + operation: 'release', + taskId, + leaseTokenDigest: tokenDigest, + expectedRevision, + }); + const replay = await this.db + .prepare( + `SELECT task_id, operation, expected_revision, request_digest, receipt_json + FROM workshop_task_lease_receipts + WHERE installation_id = ? AND principal_id = ? AND idempotency_key = ?`, + ) + .bind(context.installationId, context.principalId, idempotencyKey) + .first<{ + task_id: string; + operation: string; + expected_revision: number; + request_digest: string | null; + receipt_json: string; + }>(); + if (replay) { + if ( + replay.task_id !== taskId || + replay.operation !== 'release' || + replay.expected_revision !== expectedRevision || + replay.request_digest !== requestDigest + ) + throw new WorkshopError( + 'conflict', + 'Idempotency key was used for another mutation', + 409, + ); + return { + ...(JSON.parse(replay.receipt_json) as TaskLeaseReleaseReceipt), + replayed: true, + }; + } + const now = this.#clock().toISOString(); + const predicate = visibilitySql('workshop_tasks', context); + const current = await this.#getForMutation(taskId, context); + if (current.revision !== expectedRevision) + throw new WorkshopError('conflict', 'Task revision changed', 409); + const priorLease = await this.db + .prepare( + `SELECT lease_id FROM workshop_tasks + WHERE id = ? AND lease_token_digest = ? AND lease_owner_principal_id = ?`, + ) + .bind(taskId, tokenDigest, context.principalId) + .first<{ lease_id: string }>(); + const eventId = this.#createId(); + const { claimedAt: _claimedAt, ...withoutClaimedAt } = current; + void _claimedAt; + const next: Task = { + ...withoutClaimedAt, + claimed: false, + revision: current.revision + 1, + updatedAt: nextTimestamp(now, current.updatedAt), + authorizationRevision: context.authorizationRevision, + }; + const receipt: TaskLeaseReleaseReceipt = { + taskId, + released: true, + revision: next.revision, + replayed: false, + }; + const row = await this.#commitCanonical( + context, + next, + 'task.state', + await this.#predecessor(taskId, context), + { + sql: `UPDATE workshop_tasks + SET claimed = 0, claimed_at = NULL, lease_id = NULL, + lease_token_digest = NULL, lease_owner_principal_id = NULL, + lease_expires_at = NULL, revision = revision + 1, + authorization_revision = ?, updated_at = ?, + search_event_id = ?, search_event_sequence = NULL + WHERE id = ? AND revision = ? AND lease_token_digest = ? + AND lease_owner_principal_id = ? AND lease_expires_at > ? + AND completed_at IS NULL AND archived_at IS NULL + AND ${predicate.sql} + RETURNING *`, + values: [ + context.authorizationRevision, + next.updatedAt, + eventId, + taskId, + expectedRevision, + tokenDigest, + context.principalId, + now, + ...predicate.values, + ], + }, + eventId, + [ + { + sql: `INSERT INTO workshop_task_lease_receipts + (installation_id, principal_id, idempotency_key, task_id, operation, + expected_revision, resulting_revision, request_digest, receipt_json, created_at) + VALUES (?, ?, ?, ?, 'release', ?, ?, ?, ?, ?)`, + values: [ + context.installationId, + context.principalId, + idempotencyKey, + taskId, + expectedRevision, + next.revision, + requestDigest, + JSON.stringify(receipt), + now, + ], + }, + { + sql: `INSERT INTO workshop_task_lease_events + (event_id, installation_id, task_id, lease_id, actor_principal_id, + event_kind, task_revision, lease_expires_at, created_at) + VALUES (?, ?, ?, ?, ?, 'released', ?, NULL, ?)`, + values: [ + this.#createId(), + context.installationId, + taskId, + priorLease?.lease_id ?? taskId, + context.principalId, + next.revision, + now, + ], + }, + ], + ); + if (!row) + throw new WorkshopError( + 'conflict', + 'Lease is invalid, expired, not owned, or revision changed', + 409, + ); + return receipt; + } + async complete( id: string, resultValue: unknown, authorization: AuthorizationContext, retryOnConcurrentEdit = true, + lease?: { + tokenDigest: string; + principalId: string; + expiresAfter: string; + expectedRevision: number; + receipt?: { + idempotencyKey: string; + leaseId: string; + createdAt: string; + requestDigest: string; + }; + }, ): Promise { - const context = await this.#context(authorization, 'task-write'); + const context = await this.#context(authorization, 'task:write'); const current = await this.#getForMutation(id, context); + if (lease && current.revision !== lease.expectedRevision) + throw new WorkshopError('conflict', 'Task revision changed', 409); const predicate = visibilitySql('workshop_tasks', context); const outcome = normalizeOutcome(resultValue); const result = requiredText( @@ -755,9 +1243,12 @@ export class TaskService { { sql: `UPDATE workshop_tasks SET claimed = 0, claimed_at = NULL, completed_at = ?, result = ?, outcome_kind = ?, + lease_id = NULL, lease_token_digest = NULL, + lease_owner_principal_id = NULL, lease_expires_at = NULL, revision = ?, authorization_revision = ?, updated_at = ?, search_event_id = ?, search_event_sequence = NULL WHERE id = ? AND revision = ? AND claimed = 1 AND completed_at IS NULL AND archived_at IS NULL + ${lease ? 'AND lease_token_digest = ? AND lease_owner_principal_id = ? AND lease_expires_at > ?' : ''} AND ${predicate.sql} RETURNING *`, values: [ next.completedAt, @@ -769,10 +1260,49 @@ export class TaskService { eventId, id, current.revision, + ...(lease + ? [lease.tokenDigest, lease.principalId, lease.expiresAfter] + : []), ...predicate.values, ], }, eventId, + lease?.receipt + ? [ + { + sql: `INSERT INTO workshop_task_lease_receipts + (installation_id, principal_id, idempotency_key, task_id, operation, + expected_revision, resulting_revision, request_digest, receipt_json, created_at) + VALUES (?, ?, ?, ?, 'complete', ?, ?, ?, ?, ?)`, + values: [ + context.installationId, + context.principalId, + lease.receipt.idempotencyKey, + id, + lease.expectedRevision, + next.revision, + lease.receipt.requestDigest, + JSON.stringify(next), + lease.receipt.createdAt, + ], + }, + { + sql: `INSERT INTO workshop_task_lease_events + (event_id, installation_id, task_id, lease_id, actor_principal_id, + event_kind, task_revision, lease_expires_at, created_at) + VALUES (?, ?, ?, ?, ?, 'completed', ?, NULL, ?)`, + values: [ + this.#createId(), + context.installationId, + id, + lease.receipt.leaseId, + context.principalId, + next.revision, + lease.receipt.createdAt, + ], + }, + ] + : [], ); } catch (error) { const retried = await this.#retryCompletion( @@ -803,12 +1333,118 @@ export class TaskService { ); } + async completeLease( + input: { + taskId: unknown; + leaseToken: unknown; + expectedRevision: unknown; + idempotencyKey: unknown; + result: unknown; + outcomeKind?: unknown; + }, + authorization: AuthorizationContext, + ): Promise { + const taskId = requiredText(input.taskId, 'taskId', 256); + const idempotencyKey = requiredText( + input.idempotencyKey, + 'idempotencyKey', + 256, + ); + const expectedRevision = Number(input.expectedRevision); + if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 1) + throw new WorkshopError( + 'validation_failed', + 'expectedRevision is invalid', + 400, + ); + const context = await this.#context(authorization, 'task:write'); + const leaseToken = requiredText(input.leaseToken, 'leaseToken', 4096); + const tokenBytes = decodeLeaseToken(leaseToken); + const tokenDigest = await digestLeaseToken(tokenBytes); + tokenBytes.fill(0); + const outcome = normalizeOutcome({ + result: input.result, + kind: input.outcomeKind ?? 'success', + }); + const result = requiredText( + outcome.result, + 'result', + this.#limits.maxResultBytes, + ); + const requestDigest = await digestWorkshopOperationRequest({ + operation: 'complete', + taskId, + leaseTokenDigest: tokenDigest, + expectedRevision, + result, + outcomeKind: outcome.kind, + }); + const replay = await this.db + .prepare( + `SELECT task_id, operation, expected_revision, request_digest, receipt_json + FROM workshop_task_lease_receipts + WHERE installation_id = ? AND principal_id = ? AND idempotency_key = ?`, + ) + .bind(context.installationId, context.principalId, idempotencyKey) + .first<{ + task_id: string; + operation: string; + expected_revision: number; + request_digest: string | null; + receipt_json: string; + }>(); + if (replay) { + if ( + replay.task_id !== taskId || + replay.operation !== 'complete' || + replay.expected_revision !== expectedRevision || + replay.request_digest !== requestDigest + ) + throw new WorkshopError( + 'conflict', + 'Idempotency key was used for another mutation', + 409, + ); + return JSON.parse(replay.receipt_json) as Task; + } + const now = this.#clock().toISOString(); + const priorLease = await this.db + .prepare( + `SELECT lease_id FROM workshop_tasks + WHERE id = ? AND lease_token_digest = ? AND lease_owner_principal_id = ?`, + ) + .bind(taskId, tokenDigest, context.principalId) + .first<{ lease_id: string }>(); + const task = await this.complete( + taskId, + { + result, + kind: outcome.kind, + }, + context, + false, + { + tokenDigest, + principalId: context.principalId, + expiresAfter: now, + expectedRevision, + receipt: { + idempotencyKey, + leaseId: priorLease?.lease_id ?? taskId, + createdAt: now, + requestDigest, + }, + }, + ); + return task; + } + async resetAbandonedClaim( id: string, claimedBefore: string, authorization: AuthorizationContext, ): Promise { - const context = await this.#context(authorization, 'task-write'); + const context = await this.#context(authorization, 'task:write'); requireCapability(context, 'admin'); const current = await this.#getForMutation(id, context); const predicate = visibilitySql('workshop_tasks', context); @@ -920,16 +1556,8 @@ export class TaskService { id: string, context: AuthorizationContext, ): Promise<{ eventId: string; sequence: number }> { - const row = await this.db - .prepare( - `SELECT current_event_id AS event_id, current_event_sequence AS sequence - FROM taproot_unified_search_source_registry - WHERE installation_id = ? AND source_kind = 'task' AND source_id = ?`, - ) - .bind(context.installationId, id) - .first<{ event_id: string; sequence: number }>(); - if (!row || !Number.isSafeInteger(Number(row.sequence))) throw denied(); - return { eventId: row.event_id, sequence: Number(row.sequence) }; + if (!this.options.search) throw denied(); + return this.options.search.currentPredecessor(id, context); } async #commitCanonical( @@ -939,6 +1567,10 @@ export class TaskService { predecessor: { eventId: string; sequence: number } | null, mutation: { sql: string; values: readonly unknown[] }, eventId: string, + additionalStatements: readonly { + sql: string; + values: readonly unknown[]; + }[] = [], ): Promise { if (!this.options.search) throw new WorkshopError( @@ -962,7 +1594,11 @@ export class TaskService { { sql: `INSERT INTO workshop_task_revisions (task_id, revision, snapshot_json, actor_principal_id, event_id, created_at) - VALUES (?, ?, ?, ?, ?, ?)`, + SELECT ?, ?, ?, ?, ?, ? + WHERE EXISTS ( + SELECT 1 FROM workshop_tasks + WHERE id = ? AND revision = ? AND search_event_id = ? + )`, values: [ next.id, next.revision, @@ -970,8 +1606,12 @@ export class TaskService { context.principalId, eventId, next.updatedAt, + next.id, + next.revision, + eventId, ], }, + ...additionalStatements, ], }, ); @@ -1101,9 +1741,26 @@ export class TaskService { ); } + #serializeTaskMutation( + taskId: string, + execute: () => Promise, + ): Promise { + const prior = this.#taskMutationTails.get(taskId) ?? Promise.resolve(); + const current = prior.catch(() => undefined).then(execute); + const tail = current.then( + () => undefined, + () => undefined, + ); + this.#taskMutationTails.set(taskId, tail); + return current.finally(() => { + if (this.#taskMutationTails.get(taskId) === tail) + this.#taskMutationTails.delete(taskId); + }); + } + async #context( authorization: AuthorizationContext, - capability: 'read' | 'task-write' | 'admin' = 'read', + capability: 'read' | 'task:write' | 'admin' = 'read', ): Promise { const context = await requireCurrentAuthorization( this.options.authorization, diff --git a/src/site.ts b/src/site.ts deleted file mode 100644 index 4f1342b..0000000 --- a/src/site.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './site/handlers.js'; diff --git a/src/site/handlers.ts b/src/site/handlers.ts deleted file mode 100644 index 4b6cd25..0000000 --- a/src/site/handlers.ts +++ /dev/null @@ -1,591 +0,0 @@ -import { createWorkshopMcpServer } from '../mcp.js'; -import { createWorkshopToolDispatcher } from '../mcp/dispatcher.js'; -import { WorkshopError, normalizeWorkshopError } from '../protocol/errors.js'; -import type { WorkshopPrincipal } from '../protocol.js'; -import { - authorize, - requireCapability, - requireCurrentAuthorization, -} from '../server/authorization.js'; -import type { WorkshopRuntime } from '../server/context.js'; -import { HealthService } from '../server/health.js'; -import { resolveLimits } from '../server/limits.js'; -import { observeSafely } from '../server/observability.js'; - -export type WorkshopRouteHandler = (request: Request) => Promise; - -export function createTasksHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - return route(runtime, async (request, principal) => { - if (request.method === 'GET') { - const actor = authorize(principal, 'read'); - return runtime.tasks.list(queryObject(request), actor); - } - if (request.method === 'POST') { - const actor = authorize(principal, 'task-write'); - return runtime.tasks.create( - (await json(request, runtime)) as never, - actor, - ); - } - throw methodNotAllowed('GET, POST'); - }); -} - -export function createTaskHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - return route(runtime, async (request, principal) => { - const id = pathValue(request, 'tasks'); - if (request.method === 'GET') { - const actor = authorize(principal, 'read'); - return runtime.tasks.get(id, actor); - } - if (request.method === 'PATCH') { - const actor = authorize(principal, 'task-write'); - return runtime.tasks.update( - id, - (await json(request, runtime)) as never, - actor, - ); - } - if (request.method === 'DELETE') { - const actor = authorize(principal, 'task-write'); - const revision = request.headers.get('x-workshop-revision'); - return runtime.tasks.archive( - id, - { - ...(revision === null - ? { expectedUpdatedAt: requiredHeader(request, 'if-match') } - : { expectedRevision: Number(revision) }), - }, - actor, - ); - } - throw methodNotAllowed('GET, PATCH, DELETE'); - }); -} - -export function createTaskPacketHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - return route(runtime, async (request, principal) => { - only(request, 'GET'); - const actor = authorize(principal, 'read'); - return runtime.packets.get( - pathValue(request, 'tasks'), - actor, - request.signal, - ); - }); -} - -export function createTaskHistoryHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - return route(runtime, async (request, principal) => { - only(request, 'GET'); - const limit = queryLimit(request); - return runtime.tasks.history( - pathValue(request, 'tasks'), - authorize(principal, 'read'), - limit === undefined ? {} : { limit }, - ); - }); -} - -export function createTaskClaimHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - return route(runtime, async (request, principal) => { - only(request, 'POST'); - const actor = authorize(principal, 'task-write'); - return runtime.tasks.claim(pathValue(request, 'tasks'), actor); - }); -} - -export function createTaskCompleteHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - return route(runtime, async (request, principal) => { - only(request, 'POST'); - const actor = authorize(principal, 'task-write'); - const input = await json(request, runtime); - return runtime.tasks.complete( - pathValue(request, 'tasks'), - input.result, - actor, - ); - }); -} - -export function createMemoriesHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - return route(runtime, async (request, principal) => { - only(request, 'GET'); - const actor = authorize(principal, 'read'); - return runtime.memories.list(queryObject(request), actor); - }); -} - -export function createMemoryHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - return route(runtime, async (request, principal) => { - const slug = pathValue(request, 'memories'); - if (request.method === 'GET') { - const actor = authorize(principal, 'read'); - return runtime.memories.get(slug, actor); - } - if (request.method === 'PUT') { - const actor = authorize(principal, 'memory-write'); - return runtime.memories.upsert( - slug, - (await json(request, runtime)) as never, - actor, - ); - } - if (request.method === 'DELETE') { - await runtime.memories.delete( - slug, - Number(requiredHeader(request, 'x-workshop-revision')), - authorize(principal, 'memory-write'), - ); - return { deleted: true }; - } - throw methodNotAllowed('GET, PUT, DELETE'); - }); -} - -export function createMemoryHistoryHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - return route(runtime, async (request, principal) => { - only(request, 'GET'); - const limit = queryLimit(request); - return runtime.memories.history( - pathValue(request, 'memories'), - authorize(principal, 'read'), - limit === undefined ? {} : { limit }, - ); - }); -} - -export function createPromptsHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - return route(runtime, async (request, principal) => { - const prompts = requiredPrompts(runtime); - if (request.method === 'GET') - return prompts.list(queryObject(request), authorize(principal, 'read')); - if (request.method === 'POST') - return prompts.create( - (await json(request, runtime)) as never, - authorize(principal, 'prompt-write'), - ); - throw methodNotAllowed('GET, POST'); - }); -} - -export function createPromptHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - return route(runtime, async (request, principal) => { - const prompts = requiredPrompts(runtime); - const id = pathValue(request, 'prompts'); - if (request.method === 'GET') - return prompts.get(id, authorize(principal, 'read')); - if (request.method === 'PATCH') - return prompts.update( - id, - (await json(request, runtime)) as never, - authorize(principal, 'prompt-write'), - ); - if (request.method === 'DELETE') { - await prompts.delete( - id, - Number(requiredHeader(request, 'x-workshop-revision')), - authorize(principal, 'prompt-write'), - ); - return { deleted: true }; - } - throw methodNotAllowed('GET, PATCH, DELETE'); - }); -} - -export function createPromptHistoryHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - return route(runtime, async (request, principal) => { - only(request, 'GET'); - const limit = queryLimit(request); - return requiredPrompts(runtime).history( - pathValue(request, 'prompts'), - authorize(principal, 'read'), - limit === undefined ? {} : { limit }, - ); - }); -} - -export function createSearchHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - return route(runtime, async (request, principal) => { - only(request, 'POST'); - if (!runtime.search) - throw new WorkshopError( - 'internal_error', - 'Search integration is not registered', - ); - return runtime.search.search( - (await json(request, runtime)) as never, - authorize(principal, 'read'), - ); - }); -} - -export function createSearchAdminHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - const dispatcher = createWorkshopToolDispatcher(runtime); - return route(runtime, async (request, principal) => { - const actor = authorize(principal, 'search:admin'); - if (request.method === 'GET') { - if (!runtime.search) - throw new WorkshopError( - 'internal_error', - 'Search integration is not registered', - ); - return { - materialization: await runtime.search.materialization.health(actor), - semantic: await runtime.search.semantic.status(actor), - }; - } - only(request, 'POST'); - const result = await dispatcher.callTool( - { name: 'search_admin', arguments: await json(request, runtime) }, - { - principal: actor, - requestId: request.headers.get('x-request-id') ?? crypto.randomUUID(), - signal: request.signal, - }, - ); - if (!result.ok) - throw new WorkshopError( - result.failure.error.code, - result.failure.error.message, - undefined, - result.failure.error.details, - ); - return result.value; - }); -} - -function requiredPrompts(runtime: WorkshopRuntime) { - if (!runtime.prompts) - throw new WorkshopError( - 'internal_error', - 'Prompt producer is not registered', - ); - return runtime.prompts; -} - -export function createWorkshopMcpHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - const server = createWorkshopMcpServer(runtime); - return (request) => server.handle(request); -} - -export function createWorkshopHealthHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - const health = healthService(runtime); - return async (request) => { - try { - only(request, 'GET'); - const body = await health.publicHealth(); - return Response.json(body, { - status: body.status === 'ok' ? 200 : 503, - headers: { 'cache-control': 'no-store' }, - }); - } catch (error) { - return errorResponse(error); - } - }; -} - -export function createWorkshopProbeHandler( - runtime: WorkshopRuntime, -): WorkshopRouteHandler { - const health = healthService(runtime); - return route(runtime, async (request, principal) => { - const actor = await requireCurrentAuthorization( - runtime.authorization, - authorize(principal, 'admin'), - ); - if (request.method === 'GET') return health.inspect(); - if (request.method !== 'POST') throw methodNotAllowed('GET, POST'); - requireCapability(actor, 'task-write'); - requireCapability(actor, 'memory-write'); - const key = crypto.randomUUID(); - const slug = `probe-${key}`; - const memory = await runtime.memories.upsert( - slug, - { - description: 'Temporary Workshop semantic probe', - content: 'Probe guidance', - }, - actor, - ); - const task = await runtime.tasks.create( - { - idempotencyKey: `probe:${key}`, - description: 'Temporary Workshop semantic probe', - prompt: 'Verify the Workshop lifecycle.', - memorySlugs: [slug], - }, - actor, - ); - const packet = await runtime.packets.get(task.id, actor, request.signal); - const claimed = await runtime.tasks.claim(task.id, actor); - const completed = await runtime.tasks.complete( - task.id, - 'Workshop lifecycle probe completed.', - actor, - ); - const archiveTask = await runtime.tasks.create( - { - idempotencyKey: `probe:${key}:archive`, - description: 'Temporary Workshop archive probe', - prompt: 'Verify the Workshop archive transition.', - }, - actor, - ); - const archived = await runtime.tasks.archive( - archiveTask.id, - { - expectedUpdatedAt: archiveTask.updatedAt, - }, - actor, - ); - return { memory, task, packet, claimed, completed, archiveTask, archived }; - }); -} - -function route( - runtime: WorkshopRuntime, - handler: ( - request: Request, - principal: WorkshopPrincipal | null, - ) => Promise, -): WorkshopRouteHandler { - return async (request) => { - if (request.method === 'OPTIONS') return preflight(request, runtime); - try { - validateOrigin(request, runtime); - const principal = await runtime.resolvePrincipal(request); - const body = await handler(request, principal); - return jsonResponse(body, 200, request, runtime); - } catch (error) { - const normalized = normalizeWorkshopError(error); - if ( - normalized.code === 'unauthenticated' || - normalized.code === 'forbidden' - ) { - await observeSafely(runtime.observe, { - operation: 'authorization.failure', - outcome: 'error', - durationMs: 0, - errorCode: normalized.code, - ...(request.headers.get('x-request-id') - ? { requestId: request.headers.get('x-request-id')! } - : {}), - }); - } - return errorResponse(error, request, runtime); - } - }; -} - -function healthService(runtime: WorkshopRuntime): HealthService { - return new HealthService({ - db: runtime.db, - diamondHealth: runtime.diamondHealth, - knowledge: runtime.knowledge, - constructMcp: () => createWorkshopMcpServer(runtime), - }); -} - -async function json( - request: Request, - runtime: WorkshopRuntime, -): Promise> { - const contentType = request.headers.get('content-type')?.split(';', 1)[0]; - if (contentType !== 'application/json') { - throw new WorkshopError('bad_request', 'Expected application/json', 415); - } - const max = resolveLimits(runtime.limits).maxMcpRequestBytes; - const declared = Number(request.headers.get('content-length')); - if (Number.isFinite(declared) && declared > max) { - throw new WorkshopError('limit_exceeded', 'Request body is too large'); - } - if (!request.body) - throw new WorkshopError('bad_request', 'Missing request body'); - const reader = request.body.getReader(); - const chunks: Uint8Array[] = []; - let total = 0; - while (true) { - const next = await reader.read(); - if (next.done) break; - total += next.value.byteLength; - if (total > max) { - await reader.cancel(); - throw new WorkshopError('limit_exceeded', 'Request body is too large'); - } - chunks.push(next.value); - } - const bytes = new Uint8Array(total); - let offset = 0; - for (const chunk of chunks) { - bytes.set(chunk, offset); - offset += chunk.byteLength; - } - const text = new TextDecoder().decode(bytes); - try { - const parsed = JSON.parse(text) as unknown; - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - throw new Error('object required'); - } - return parsed as Record; - } catch { - throw new WorkshopError('bad_request', 'Invalid JSON object body'); - } -} - -function queryObject(request: Request): Record { - return Object.fromEntries(new URL(request.url).searchParams); -} - -function queryLimit(request: Request): number | undefined { - const value = new URL(request.url).searchParams.get('limit'); - return value === null ? undefined : Number(value); -} - -function pathValue(request: Request, segment: string): string { - const parts = new URL(request.url).pathname.split('/').filter(Boolean); - const index = parts.lastIndexOf(segment); - const value = parts[index + 1]; - if (!value) { - throw new WorkshopError( - 'bad_request', - `Missing path value after ${segment}`, - ); - } - return decodeURIComponent(value); -} - -function only(request: Request, method: string): void { - if (request.method !== method) throw methodNotAllowed(method); -} - -function requiredHeader(request: Request, name: string): string { - const value = request.headers.get(name)?.trim(); - if (!value) { - throw new WorkshopError( - 'bad_request', - `The ${name} header is required`, - 400, - { field: name }, - ); - } - return value; -} - -function methodNotAllowed(allow: string): WorkshopError { - return new WorkshopError( - 'bad_request', - `Method not allowed; use ${allow}`, - 405, - { - allow, - }, - ); -} - -function validateOrigin(request: Request, runtime: WorkshopRuntime): void { - const origin = request.headers.get('origin'); - if (!origin || origin === new URL(request.url).origin) return; - const allowed = - typeof runtime.corsOrigin === 'function' - ? runtime.corsOrigin(request) - : runtime.corsOrigin; - if (origin !== allowed) - throw new WorkshopError('forbidden', 'Origin is not allowed'); -} - -function cors(request: Request, runtime: WorkshopRuntime): HeadersInit { - const origin = request.headers.get('origin'); - const allowed = - typeof runtime.corsOrigin === 'function' - ? runtime.corsOrigin(request) - : runtime.corsOrigin; - return origin && - (origin === new URL(request.url).origin || origin === allowed) - ? { 'access-control-allow-origin': origin, vary: 'Origin' } - : {}; -} - -function preflight(request: Request, runtime: WorkshopRuntime): Response { - try { - validateOrigin(request, runtime); - return new Response(null, { - status: 204, - headers: { - ...cors(request, runtime), - 'access-control-allow-methods': - 'GET, POST, PUT, PATCH, DELETE, OPTIONS', - 'access-control-allow-headers': - 'Authorization, Content-Type, If-Match, X-Workshop-Revision, X-Request-Id', - }, - }); - } catch (error) { - return errorResponse(error, request, runtime); - } -} - -function jsonResponse( - body: unknown, - status: number, - request: Request, - runtime: WorkshopRuntime, -): Response { - return Response.json(body, { - status, - headers: { - ...cors(request, runtime), - 'cache-control': 'no-store', - 'x-content-type-options': 'nosniff', - }, - }); -} - -function errorResponse( - error: unknown, - request?: Request, - runtime?: WorkshopRuntime, -): Response { - const normalized = normalizeWorkshopError(error); - return Response.json( - { error: normalized.toJSON() }, - { - status: normalized.status, - headers: { - ...(request && runtime ? cors(request, runtime) : {}), - 'cache-control': 'no-store', - }, - }, - ); -} diff --git a/src/taproot-operation.ts b/src/taproot-operation.ts new file mode 100644 index 0000000..d2df7c7 --- /dev/null +++ b/src/taproot-operation.ts @@ -0,0 +1,791 @@ +import { + createAuthorizedTaproot, + createTaprootWorkshopPortV1, + TaprootContentRepositoryV1, + type AuthorizationContext, + type AuthorizationCursorCodec, + type CreateAnnotationInputV1, + type CreateItemInput, + type CreatePropertyInput, + type CreateResourceInputV1, + type D1DatabaseLike, + type EntityCommand, + type EntityId, + type PortableResourcePayloadStoreV1, + type TaprootHostWriteCapability, + type TaprootOptions, + type TaprootWorkshopPortV1, +} from '@gnolith/taproot'; +import { + createAuthorizedSearchServiceV1, + createSearchMaterializationAdminGuardV1, + type AuthorizedSearchServiceV1, + type SearchMaterializationAdminGuardV1, + type SearchKind, + type SearchResultV1, + type SemanticSearchAdminV1, + type SearchMaterializationHealthV1, +} from '@gnolith/taproot/search'; +import { prepareScopedSparqlPolicyV1 } from '@gnolith/taproot/sparql-policy'; +import type { OperationName } from './generated/operations.js'; +import type { + OperationCallContext, + TaprootOperationPort, + WorkshopStatusSnapshot, +} from './operations.js'; +import type { AuthenticatedPrincipal } from './protocol.js'; +import { WorkshopError } from './protocol/errors.js'; +import type { WorkshopAuthorizedSearchServiceV1 } from './server/search.js'; +import type { D1DatabaseLike as WorkshopD1DatabaseLike } from './server/database.js'; +import { + createWorkshopAuthorizationAdministrationV1, + type WorkshopAuthorizationAdministrationV1, +} from './authorization-administration.js'; +import { configureWorkshopSemanticsV1 } from './semantic-configuration.js'; + +export interface WorkshopTaprootAssemblyOptionsV1 { + kind: 'workshop-taproot-assembly-v1'; + db: D1DatabaseLike; + installationId: string; + taprootOptions: TaprootOptions; + hostAuthority: TaprootHostWriteCapability; + cursorCodec: AuthorizationCursorCodec; + payloadStore?: PortableResourcePayloadStoreV1; + maxHydratedResourceBytes?: number; + semantic?: SemanticSearchAdminV1; + search?: WorkshopAuthorizedSearchServiceV1; + materialization?: SearchMaterializationAdminGuardV1; + authorizationAdministration?: WorkshopAuthorizationAdministrationV1; + status( + context: AuthorizationContext, + signal: AbortSignal, + ): Promise; +} + +interface TaprootRuntimeV1 { + operator: TaprootWorkshopPortV1; + content: TaprootContentRepositoryV1; + search: AuthorizedSearchServiceV1; + materialization: SearchMaterializationAdminGuardV1; + authorizationAdministration: WorkshopAuthorizationAdministrationV1; +} + +/** Composes only packed Taproot public capabilities into Workshop's catalog. */ +export async function createWorkshopTaprootOperationPortV1( + options: WorkshopTaprootAssemblyOptionsV1, +): Promise { + validateAssembly(options); + const content = new TaprootContentRepositoryV1(options.db, { + installationId: options.installationId, + ...(options.payloadStore ? { payloadStore: options.payloadStore } : {}), + }); + const runtime: TaprootRuntimeV1 = { + operator: await createTaprootWorkshopPortV1( + options.db, + options.taprootOptions, + options.hostAuthority, + { + installationId: options.installationId, + ...(options.payloadStore ? { payloadStore: options.payloadStore } : {}), + ...(options.maxHydratedResourceBytes === undefined + ? {} + : { + maxHydratedResourceBytes: options.maxHydratedResourceBytes, + }), + }, + ), + content, + search: options.search + ? (options.search as unknown as AuthorizedSearchServiceV1) + : createAuthorizedSearchServiceV1(options.db, { + installationId: options.installationId, + content, + ...(options.semantic ? { semantic: options.semantic } : {}), + }), + materialization: + options.materialization ?? + (await createSearchMaterializationAdminGuardV1( + options.db, + options.taprootOptions, + options.hostAuthority, + options.payloadStore + ? { payloadStore: options.payloadStore } + : undefined, + )), + authorizationAdministration: + options.authorizationAdministration ?? + (await createWorkshopAuthorizationAdministrationV1({ + db: options.db as unknown as WorkshopD1DatabaseLike, + taprootOptions: options.taprootOptions, + hostAuthority: options.hostAuthority, + installationId: options.installationId, + })), + }; + if (runtime.operator.kind !== 'taproot-workshop-port-v1') + throw unavailable('Taproot Workshop operator port is incompatible'); + + const port: TaprootOperationPort = { + async recheckPrincipal(principal, signal) { + signal.throwIfAborted(); + let live: Awaited< + ReturnType + >; + try { + live = await runtime.operator.resolveAuthorization({ + principalId: principal.principalId, + activeWorkspaceId: principal.activeWorkspaceId, + }); + } catch (error) { + if ( + (error as { name?: unknown }).name === 'AuthorizationDeniedError' || + (error as { code?: unknown }).code === 'AUTHORIZATION_DENIED' + ) + throw new WorkshopError('forbidden', 'Authorization denied', 403); + throw error; + } + assertInstallation(options.installationId, live.installationId); + const resolved = { + ...live, + credentialId: principal.credentialId, + }; + await runtime.authorizationAdministration.assertPrincipalActive(resolved); + return resolved; + }, + status(principal, signal) { + return options.status(authorization(principal), signal); + }, + createScopedSparqlView(context) { + return prepareScopedSparqlPolicyV1( + options.db, + { + installationId: options.installationId, + baseIri: options.taprootOptions.baseIri, + }, + authorization(context.principal), + ); + }, + call(name, input, context) { + return callTaproot(runtime, options, name, input, context); + }, + }; + return Object.freeze(port); +} + +async function callTaproot( + runtime: TaprootRuntimeV1, + options: WorkshopTaprootAssemblyOptionsV1, + name: OperationName, + input: Readonly>, + callContext: OperationCallContext, +): Promise { + const context = authorization(callContext.principal); + const reader = createAuthorizedTaproot( + options.db, + options.taprootOptions, + context, + { cursorCodec: options.cursorCodec }, + ); + switch (name) { + case 'authorization_admin': + return runtime.authorizationAdministration.call(input, callContext); + case 'search': { + let page: Awaited>; + try { + page = await runtime.search.search( + { + text: requiredString(input.text), + ...(Array.isArray(input.kinds) + ? { kinds: input.kinds as SearchKind[] } + : {}), + ...(typeof input.limit === 'number' ? { limit: input.limit } : {}), + ...(typeof input.cursor === 'string' + ? { cursor: input.cursor } + : {}), + }, + context, + ); + } catch (error) { + if (typeof input.cursor === 'string') + throw new WorkshopError( + 'invalid_cursor', + 'Search cursor is invalid', + 400, + ); + throw error; + } + return { + items: page.results.map((result) => ({ + ...result, + id: result.sourceId, + revision: numericRevision(result.sourceRevision), + })), + cursor: page.cursor ?? null, + }; + } + case 'search_hydrate': + return runtime.search.hydrate(hydrationIdentity(input), context); + case 'search_admin': + return searchAdministration( + runtime, + options, + input, + context, + callContext, + ); + case 'get_entity': + return reader.getEntity(requiredEntityId(input.entityId)); + case 'get_entities': + return { + items: await Promise.all( + (input.entityIds as unknown[]).map((id) => + reader.getEntity(requiredEntityId(id)), + ), + ), + }; + case 'entity_history': + return reader.listEntityRevisions(requiredEntityId(input.entityId)); + case 'entity_revision': + return reader.getEntityRevision( + requiredEntityId(input.entityId), + Number(input.revision), + ); + case 'statement_history': + return runtime.operator.statementHistory( + requiredString(input.statementId), + {}, + context, + ); + case 'statement_revision': + return runtime.operator.statementRevision( + requiredString(input.statementId), + Number(input.revision), + context, + ); + case 'export_entity_json': { + const value = await reader.getEntity(requiredEntityId(input.entityId)); + return { entityId: input.entityId, value }; + } + case 'create_item': + return runtime.operator.createItem( + createEntityInput(input), + { idempotencyKey: requiredString(input.idempotencyKey) }, + context, + ); + case 'create_property': + return runtime.operator.createProperty( + createEntityInput(input), + { idempotencyKey: requiredString(input.idempotencyKey) }, + context, + ); + case 'mutate_entity': + return runtime.operator.mutateEntity( + requiredEntityId(input.entityId), + mutations(input.mutations), + { + expectedRevision: Number(input.expectedRevision), + idempotencyKey: requiredString(input.idempotencyKey), + }, + context, + ); + case 'create_resource': + return runtime.operator.createResource( + domainInput(input.data), + { idempotencyKey: requiredString(input.idempotencyKey) }, + context, + ); + case 'get_resource': + return runtime.content.getResource( + requiredString(input.resourceId), + context, + ); + case 'update_resource': { + const resourceId = requiredString(input.resourceId); + const current = await runtime.content.getResource(resourceId, context); + return runtime.operator.updateResource( + resourceId, + Number(input.expectedRevision), + resourceUpdateInput(current, input.patch), + { idempotencyKey: requiredString(input.idempotencyKey) }, + context, + ); + } + case 'delete_resource': + return runtime.operator.deleteResource( + requiredString(input.resourceId), + Number(input.expectedRevision), + { idempotencyKey: requiredString(input.idempotencyKey) }, + context, + ); + case 'hydrate_resource': { + const resourceId = requiredString(input.resourceId); + const revision = Number(input.revision); + const bytes = await runtime.operator.hydrateResource( + resourceId, + revision, + context, + callContext.signal, + ); + return { + resourceId, + revision, + encoding: 'base64', + byteLength: bytes.byteLength, + data: Buffer.from(bytes).toString('base64'), + }; + } + case 'resource_history': + return runtime.operator.resourceHistory( + requiredString(input.resourceId), + {}, + context, + ); + case 'resource_revision': + return runtime.operator.resourceRevision( + requiredString(input.resourceId), + Number(input.revision), + context, + ); + case 'create_annotation': + return runtime.operator.createAnnotation( + domainInput(input.data), + { idempotencyKey: requiredString(input.idempotencyKey) }, + context, + ); + case 'get_annotation': + return runtime.content.getAnnotation( + requiredString(input.annotationId), + context, + ); + case 'update_annotation': { + const annotationId = requiredString(input.annotationId); + const current = await runtime.content.getAnnotation( + annotationId, + context, + ); + return runtime.operator.updateAnnotation( + annotationId, + Number(input.expectedRevision), + annotationUpdateInput(current, input.patch), + { idempotencyKey: requiredString(input.idempotencyKey) }, + context, + ); + } + case 'delete_annotation': + return runtime.operator.deleteAnnotation( + requiredString(input.annotationId), + Number(input.expectedRevision), + { idempotencyKey: requiredString(input.idempotencyKey) }, + context, + ); + case 'annotation_history': + return runtime.operator.annotationHistory( + requiredString(input.annotationId), + {}, + context, + ); + case 'annotation_revision': + return runtime.operator.annotationRevision( + requiredString(input.annotationId), + Number(input.revision), + context, + ); + default: + throw new WorkshopError('not_found', 'Operation not found', 404); + } +} + +async function searchAdministration( + runtime: TaprootRuntimeV1, + options: WorkshopTaprootAssemblyOptionsV1, + input: Readonly>, + context: AuthorizationContext, + callContext: OperationCallContext, +): Promise { + callContext.signal.throwIfAborted(); + const action = requiredString(input.action); + const values = record(input.options ?? {}); + const limit = typeof input.limit === 'number' ? input.limit : 100; + switch (action) { + case 'health': + return { + materialization: truthfulMaterializationHealth( + await runtime.materialization.health(context), + ), + semantic: options.semantic + ? await options.semantic.status(context) + : { state: 'unconfigured' }, + }; + case 'materialize': { + const receipt = await runBoundedMaterialization( + runtime.materialization, + context, + { + limit, + maxRebuildRoots: optionalInteger(values.maxRebuildRoots, limit), + ...(typeof values.maxChunkBytes === 'number' + ? { maxChunkBytes: values.maxChunkBytes } + : {}), + }, + ); + const health = truthfulMaterializationHealth( + await runtime.materialization.health(context), + ); + return { + claimed: receipt.claimed, + completed: receipt.completed, + superseded: receipt.superseded, + deferred: receipt.deferred, + dead: receipt.dead, + rebuildRootsEnumerated: receipt.rebuildRootsEnumerated, + job: { + state: 'completed', + requestedLimit: limit, + }, + result: { ...receipt }, + health, + }; + } + case 'retry-dead': + return { + retried: await runtime.materialization.retryDead(context, { limit }), + }; + case 'rebuild': + return { + generation: await runtime.materialization.startShadowRebuild(context), + }; + case 'activate': + return { + generation: await runtime.materialization.activateReadyShadow(context), + }; + } + const semantic = options.semantic; + if (!semantic) throw unavailable('Semantic search is not configured'); + switch (action) { + case 'configure': + return configureWorkshopSemanticsV1( + { + version: 1, + expectedRevision: Number(values.expectedRevision), + idempotencyKey: requiredString(values.idempotencyKey), + configuration: values.configuration as never, + }, + { + db: options.db as unknown as WorkshopD1DatabaseLike, + installationId: options.installationId, + semantic, + authorization: context, + }, + ); + case 'semantic-select': + await semantic.select(requiredString(values.configurationId), context); + return semantic.status(context); + case 'semantic-reconnect': + return { + connected: await semantic.reconnect( + requiredString(values.configurationId), + context, + ), + }; + case 'semantic-estimate': + return semantic.estimate( + requiredString(values.configurationId), + values.policy as never, + context, + ); + case 'semantic-approve': + await semantic.approve(requiredString(values.planId), context); + return semantic.status(context); + case 'semantic-run': + return semantic.run( + requiredString(values.planId), + context, + callContext.signal, + ); + case 'semantic-resume': + return semantic.resume( + requiredString(values.planId), + context, + callContext.signal, + ); + case 'semantic-pause': + await semantic.pause(requiredString(values.planId), context); + return semantic.status(context); + case 'semantic-stop': + await semantic.stop(requiredString(values.planId), context); + return semantic.status(context); + case 'semantic-retry': + await semantic.retry(requiredString(values.planId), context); + return semantic.status(context); + case 'semantic-exclude': + await semantic.exclude( + requiredString(values.configurationId), + Number(values.generation), + requiredString(values.derivedId), + requiredString(values.reason), + context, + ); + return semantic.status(context); + case 'semantic-retire': + await semantic.retire(requiredString(values.configurationId), context); + return semantic.status(context); + case 'semantic-delete': + await semantic.deleteEmbeddings( + requiredString(values.configurationId), + context, + ); + return semantic.status(context); + default: + throw new WorkshopError( + 'validation_failed', + 'Search action is invalid', + 400, + ); + } +} + +async function runBoundedMaterialization( + materialization: SearchMaterializationAdminGuardV1, + context: AuthorizationContext, + options: { + limit: number; + maxRebuildRoots: number; + maxChunkBytes?: number; + }, +) { + const total = { + claimed: 0, + completed: 0, + superseded: 0, + deferred: 0, + dead: 0, + rebuildRootsEnumerated: 0, + }; + let remainingJobs = options.limit; + let remainingRebuildRoots = options.maxRebuildRoots; + while (remainingJobs > 0 && remainingRebuildRoots > 0) { + const jobBudget = Math.min(100, remainingJobs); + const rebuildBudget = Math.min(100, remainingRebuildRoots); + const receipt = await materialization.run(context, { + // Taproot deliberately bounds each owner call to 100 jobs/roots. The + // public Workshop limit is a total budget, so larger requests are + // fulfilled through repeated bounded calls rather than forwarded as one + // invalid Taproot run. + maxJobs: jobBudget, + maxRebuildRoots: rebuildBudget, + ...(options.maxChunkBytes === undefined + ? {} + : { maxChunkBytes: options.maxChunkBytes }), + }); + total.claimed += receipt.claimed; + total.completed += receipt.completed; + total.superseded += receipt.superseded; + total.deferred += receipt.deferred; + total.dead += receipt.dead; + total.rebuildRootsEnumerated += receipt.rebuildRootsEnumerated; + remainingJobs -= jobBudget; + remainingRebuildRoots -= rebuildBudget; + if (receipt.claimed === 0 && receipt.rebuildRootsEnumerated === 0) break; + } + return total; +} + +export function truthfulMaterializationHealth( + health: SearchMaterializationHealthV1, +): SearchMaterializationHealthV1 { + if ( + health.status === 'healthy' && + health.activeAppliedWatermark < health.sourceHighWatermark + ) + return { ...health, status: 'building' }; + return health; +} + +function authorization( + principal: AuthenticatedPrincipal, +): AuthorizationContext { + if (!principal.workspaceIds) + throw unavailable('Live Taproot authorization context is unavailable'); + return { + installationId: principal.installationId, + principalId: principal.principalId, + activeWorkspaceId: principal.activeWorkspaceId, + workspaceIds: principal.workspaceIds, + capabilities: principal.capabilities, + authorizationRevision: principal.authorizationRevision, + }; +} + +function createEntityInput(input: Readonly>): T { + const data = domainInput>(input.data); + return { + ...data, + editSummary: requiredString(input.authoredText), + } as T; +} + +function mutations(value: unknown) { + if (!Array.isArray(value)) throw invalid('Entity mutations are invalid'); + return value.map((entry) => { + const mutation = record(entry); + const command = domainInput(mutation.value); + const kind = requiredString(mutation.kind); + if (!commandMatchesKind(command.type, kind)) + throw invalid('Entity mutation kind does not match its command'); + return { + command, + authoredText: requiredString(mutation.authoredText), + }; + }); +} + +function commandMatchesKind( + type: EntityCommand['type'], + kind: string, +): boolean { + const prefix = type.includes('label') + ? 'label' + : type.includes('description') + ? 'description' + : type.includes('alias') + ? 'alias' + : type.includes('sitelink') + ? 'sitelink' + : type.includes('statement-rank') + ? 'rank' + : type.includes('statement') + ? 'statement' + : type.includes('qualifier') + ? 'qualifier' + : type.includes('reference') + ? 'reference' + : ''; + return prefix === kind; +} + +function hydrationIdentity( + input: Readonly>, +): SearchResultV1 { + const shared = { + sourceId: requiredString(input.id), + sourceRevision: String(input.revision), + score: 0, + snippet: '', + }; + if (input.kind === 'item') { + if (input.entityType !== 'item' && input.entityType !== 'property') + throw invalid('Item search identity requires entityType'); + return { ...shared, kind: 'item', entityType: input.entityType }; + } + return { ...shared, kind: input.kind as Exclude }; +} + +function resourceUpdateInput( + current: Awaited>, + patchValue: unknown, +): CreateResourceInputV1 { + const patch = domainInput>(patchValue); + return { + ...(current.title ? { title: current.title } : {}), + payload: current.payload, + mediaType: current.mediaType, + ...(current.language ? { language: current.language } : {}), + integrity: current.integrity, + ...patch, + id: current.id, + itemId: current.itemId, + }; +} + +function annotationUpdateInput( + current: Awaited>, + patchValue: unknown, +): CreateAnnotationInputV1 { + const patch = domainInput>(patchValue); + return { + body: current.body, + target: current.target, + ...(current.motivation ? { motivation: current.motivation } : {}), + ...(current.creator ? { creator: current.creator } : {}), + ...(current.generator ? { generator: current.generator } : {}), + ...(current.language ? { language: current.language } : {}), + ...(current.mediaType ? { mediaType: current.mediaType } : {}), + ...patch, + id: current.id, + }; +} + +function domainInput(value: unknown): T { + const input = record(value); + for (const key of [ + 'authorization', + 'authorizationContext', + 'installationId', + 'ownerPrincipalId', + 'workspaceId', + 'policyRevision', + 'authorizationRevision', + 'actor', + 'attribution', + 'requestId', + ]) { + if (key in input) + throw invalid('Identity, policy, and attribution are server-owned'); + } + return { ...input } as T; +} + +function validateAssembly(options: WorkshopTaprootAssemblyOptionsV1): void { + if ( + options.kind !== 'workshop-taproot-assembly-v1' || + !options.installationId || + !options.db || + !options.hostAuthority || + !options.cursorCodec || + typeof options.status !== 'function' + ) + throw invalid('Taproot assembly is incomplete'); +} + +function requiredEntityId(value: unknown): EntityId { + const id = requiredString(value); + if (!/^[PQ][1-9][0-9]*$/u.test(id)) throw invalid('Entity id is invalid'); + return id as EntityId; +} + +function requiredString(value: unknown): string { + if (typeof value !== 'string' || !value) + throw invalid('Required text is invalid'); + return value; +} + +function numericRevision(value: string): number { + const revision = Number(value); + if (!Number.isSafeInteger(revision) || revision < 1) + throw unavailable('Search result revision is incompatible'); + return revision; +} + +function optionalInteger(value: unknown, fallback: number): number { + return Number.isSafeInteger(value) && Number(value) > 0 + ? Number(value) + : fallback; +} + +function record(value: unknown): Readonly> { + if (!value || typeof value !== 'object' || Array.isArray(value)) + throw invalid('Expected an object'); + return value as Readonly>; +} + +function assertInstallation(expected: string, actual: string): void { + if (expected !== actual) + throw unavailable('Taproot authorization installation is incompatible'); +} + +function invalid(message: string): WorkshopError { + return new WorkshopError('validation_failed', message, 400); +} + +function unavailable(message: string): WorkshopError { + return new WorkshopError('dependency_unavailable', message, 503); +} diff --git a/src/ui.ts b/src/ui.ts deleted file mode 100644 index e509b37..0000000 --- a/src/ui.ts +++ /dev/null @@ -1,4 +0,0 @@ -'use client'; - -export * from './ui/plugin.js'; -export * from './ui/components.js'; diff --git a/src/ui/components.tsx b/src/ui/components.tsx deleted file mode 100644 index 10d712c..0000000 --- a/src/ui/components.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export * from './dashboard.js'; -export * from './tasks.js'; -export * from './task-packet.js'; -export * from './task-controls.js'; -export * from './memories.js'; -export * from './mcp-status.js'; -export * from './configuration.js'; -export * from './dashboard-screen.js'; -export * from './entity-tasks-screen.js'; -export * from './error-notice.js'; -export * from './loading-status.js'; -export * from './mcp-status-screen.js'; -export * from './memories-screen.js'; -export * from './task-workspace.js'; -export * from './tasks-screen.js'; -export * from './use-workshop-client.js'; -export type { ContextQuery } from '../protocol/tasks.js'; diff --git a/src/ui/configuration.ts b/src/ui/configuration.ts deleted file mode 100644 index 69080c5..0000000 --- a/src/ui/configuration.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { WorkshopClient } from '../protocol/client.js'; -import { hasWorkshopCapability, type WorkshopCapability } from '../protocol.js'; - -export type WorkshopClientSource = WorkshopClient | (() => WorkshopClient); - -export interface WorkshopMcpStatus { - status: 'connected' | 'unavailable' | 'unauthorized' | 'unknown'; - endpoint?: string; - detail?: string; -} - -export interface WorkshopUiOptions { - client?: WorkshopClientSource; - capabilities?: readonly WorkshopCapability[]; - loadMcpStatus?: () => Promise; -} - -export function hasUiCapability( - capabilities: readonly WorkshopCapability[], - capability: WorkshopCapability, -): boolean { - return hasWorkshopCapability(capabilities, capability); -} diff --git a/src/ui/dashboard-screen.tsx b/src/ui/dashboard-screen.tsx deleted file mode 100644 index dc36aba..0000000 --- a/src/ui/dashboard-screen.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useId } from 'react'; -import useSWR from 'swr'; -import type { WorkshopCapability } from '../protocol.js'; -import type { Memory } from '../protocol/memories.js'; -import type { Task } from '../protocol/tasks.js'; -import type { WorkshopClientSource } from './configuration.js'; -import { WorkshopDashboard } from './dashboard.js'; -import { WorkshopErrorNotice } from './error-notice.js'; -import { WorkshopLoadingStatus } from './loading-status.js'; -import { useWorkshopClient } from './use-workshop-client.js'; - -export function WorkshopDashboardScreen({ - client: clientSource, - capabilities, -}: { - client?: WorkshopClientSource; - capabilities: readonly WorkshopCapability[]; -}) { - const client = useWorkshopClient(clientSource); - const instance = useId(); - const canRead = capabilities.includes('read'); - const { data, error, isLoading } = useSWR< - { tasks: Task[]; memories: Memory[] }, - Error - >(canRead ? `workshop-dashboard:${instance}` : null, async () => { - const [tasks, memories] = await Promise.all([ - client.tasks.list({ limit: 200 }), - client.memories.list({ limit: 200 }), - ]); - return { tasks: tasks.items, memories: memories.items }; - }); - if (!canRead) { - return ( -

- Read access is required to load the Workshop dashboard. -

- ); - } - if (isLoading) - return ; - if (error) return ; - return ( - - ); -} diff --git a/src/ui/dashboard.tsx b/src/ui/dashboard.tsx deleted file mode 100644 index 885dcce..0000000 --- a/src/ui/dashboard.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { Memory } from '../protocol/memories.js'; -import type { Task } from '../protocol/tasks.js'; - -export function WorkshopDashboard({ - tasks, - memories, -}: { - tasks: readonly Task[]; - memories: readonly Memory[]; -}) { - const active = tasks.filter((task) => !task.completedAt && !task.archivedAt); - return ( -
-

Research Workshop

-
- - task.claimed).length} - /> - -
-
- ); -} - -function Stat({ label, value }: { label: string; value: number }) { - return ( -
- {value} - {label} -
- ); -} diff --git a/src/ui/entity-tasks-screen.tsx b/src/ui/entity-tasks-screen.tsx deleted file mode 100644 index 9d18bd5..0000000 --- a/src/ui/entity-tasks-screen.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { useId } from 'react'; -import useSWR from 'swr'; -import type { WorkshopCapability } from '../protocol.js'; -import type { TaskPage } from '../protocol/tasks.js'; -import type { WorkshopClientSource } from './configuration.js'; -import { hasUiCapability } from './configuration.js'; -import { WorkshopErrorNotice } from './error-notice.js'; -import { WorkshopLoadingStatus } from './loading-status.js'; -import { TaskList } from './tasks.js'; -import { useWorkshopClient } from './use-workshop-client.js'; - -export function WorkshopEntityTasksScreen({ - client: clientSource, - capabilities, - entityId, -}: { - client?: WorkshopClientSource; - capabilities: readonly WorkshopCapability[]; - entityId?: string; -}) { - const client = useWorkshopClient(clientSource); - const instance = useId(); - const tasks = useSWR( - entityId && hasUiCapability(capabilities, 'read') - ? `workshop-entity-tasks:${instance}:${entityId}` - : null, - () => client.tasks.search({ text: entityId ?? '', limit: 25 }), - ); - if (!entityId) { - return ( -

- Select an entity to inspect related tasks. -

- ); - } - if (!hasUiCapability(capabilities, 'read')) { - return ( -

- Read access is required to inspect related tasks. -

- ); - } - if (tasks.isLoading) - return ; - if (tasks.error) return ; - return ; -} diff --git a/src/ui/error-notice.tsx b/src/ui/error-notice.tsx deleted file mode 100644 index 8e630a3..0000000 --- a/src/ui/error-notice.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { WorkshopError } from '../protocol/errors.js'; - -export function WorkshopErrorNotice({ error }: { error: unknown }) { - const normalized = - error instanceof WorkshopError - ? error - : new WorkshopError( - 'internal_error', - error instanceof Error ? error.message : 'Workshop request failed', - ); - return ( -
- {normalized.code.replace(/_/gu, ' ')}:{' '} - {normalized.message} - {normalized.code === 'conflict' ? ( - Refresh current data before retrying. - ) : null} -
- ); -} diff --git a/src/ui/loading-status.tsx b/src/ui/loading-status.tsx deleted file mode 100644 index 248527a..0000000 --- a/src/ui/loading-status.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export function WorkshopLoadingStatus({ label }: { label: string }) { - return ( -

- {label} -

- ); -} diff --git a/src/ui/mcp-status-screen.tsx b/src/ui/mcp-status-screen.tsx deleted file mode 100644 index 9f4ba7b..0000000 --- a/src/ui/mcp-status-screen.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useId } from 'react'; -import useSWR from 'swr'; -import type { WorkshopMcpStatus } from './configuration.js'; -import { WorkshopErrorNotice } from './error-notice.js'; -import { WorkshopLoadingStatus } from './loading-status.js'; -import { McpStatusPanel } from './mcp-status.js'; - -const unconfigured: WorkshopMcpStatus = { - status: 'unknown', - detail: 'The Site host has not configured an MCP status check.', -}; - -export function WorkshopMcpStatusScreen({ - loadStatus, -}: { - loadStatus?: () => Promise; -}) { - const instance = useId(); - const status = useSWR( - loadStatus ? `workshop-mcp-status:${instance}` : null, - async () => { - if (loadStatus === undefined) { - throw new Error('No MCP status loader is configured.'); - } - return loadStatus(); - }, - ); - if (!loadStatus) return ; - if (status.isLoading) - return ; - if (status.error) return ; - return ( -
- - -
- ); -} diff --git a/src/ui/mcp-status.tsx b/src/ui/mcp-status.tsx deleted file mode 100644 index 1b8b2e7..0000000 --- a/src/ui/mcp-status.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export function McpStatusPanel({ - status, - endpoint, - detail, -}: { - status: 'connected' | 'unavailable' | 'unauthorized' | 'unknown'; - endpoint?: string; - detail?: string; -}) { - return ( -
-

MCP endpoint

-

- - {status} - -

- {endpoint ? {endpoint} : null} - {detail ?

{detail}

: null} -
- ); -} diff --git a/src/ui/memories-screen.tsx b/src/ui/memories-screen.tsx deleted file mode 100644 index 678d156..0000000 --- a/src/ui/memories-screen.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { useDeferredValue, useId, useState } from 'react'; -import useSWR from 'swr'; -import type { WorkshopCapability } from '../protocol.js'; -import type { - Memory, - MemoryPage, - UpsertMemoryInput, -} from '../protocol/memories.js'; -import type { WorkshopClientSource } from './configuration.js'; -import { hasUiCapability } from './configuration.js'; -import { WorkshopErrorNotice } from './error-notice.js'; -import { WorkshopLoadingStatus } from './loading-status.js'; -import { MemoryEditor, MemoryList } from './memories.js'; -import { useWorkshopClient } from './use-workshop-client.js'; - -export function WorkshopMemoriesScreen({ - client: clientSource, - capabilities, -}: { - client?: WorkshopClientSource; - capabilities: readonly WorkshopCapability[]; -}) { - const client = useWorkshopClient(clientSource); - const instance = useId(); - const [text, setText] = useState(''); - const [creating, setCreating] = useState(false); - const [newSlug, setNewSlug] = useState(''); - const [selected, setSelected] = useState(); - const [actionError, setActionError] = useState(); - const [busy, setBusy] = useState(false); - const deferredText = useDeferredValue(text); - const canRead = hasUiCapability(capabilities, 'read'); - const canWrite = hasUiCapability(capabilities, 'memory-write'); - const memories = useSWR( - canRead ? `workshop-memories:${instance}:${deferredText}` : null, - () => - client.memories.list({ - ...(deferredText.trim() ? { text: deferredText.trim() } : {}), - limit: 100, - }), - ); - if (!canRead) { - return ( -

- Read access is required to browse Workshop memories. -

- ); - } - const save = async ( - slug: string, - input: UpsertMemoryInput, - expectedUpdatedAt?: string, - ) => { - setBusy(true); - setActionError(undefined); - try { - await client.memories.upsert(slug, { - ...input, - ...(expectedUpdatedAt ? { expectedUpdatedAt } : {}), - }); - setCreating(false); - setSelected(undefined); - setNewSlug(''); - await memories.mutate(); - } catch (error) { - setActionError(error); - } finally { - setBusy(false); - } - }; - return ( -
-
-

Reusable memories

- {canWrite ? ( - - ) : null} -
- {!canWrite ? ( -

- You have read-only access to reusable guidance. -

- ) : null} - {actionError ? : null} - {creating ? ( -
- - save(newSlug.trim(), input)} - /> -
- ) : null} - {selected ? ( -
-
-

Edit {selected.slug}

- -
- save(selected.slug, input, selected.updatedAt)} - /> -
- ) : null} - - {memories.isLoading ? ( - - ) : memories.error ? ( - - ) : ( - - )} -
- ); -} diff --git a/src/ui/memories.tsx b/src/ui/memories.tsx deleted file mode 100644 index f8decbd..0000000 --- a/src/ui/memories.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { useState } from 'react'; -import type { Memory, UpsertMemoryInput } from '../protocol/memories.js'; -import { Empty, PermissionNotice, SafeText } from './primitives.js'; - -export function MemoryList({ - memories, - onSelect, -}: { - memories: readonly Memory[]; - onSelect?: (memory: Memory) => void; -}) { - return ( -
- {memories.length ? ( - memories.map((memory) => ( - - )) - ) : ( - No memories yet. - )} -
- ); -} - -export function MemoryCard({ - memory, - onSelect, -}: { - memory: Memory; - onSelect?: (memory: Memory) => void; -}) { - return ( -
-

{memory.slug}

-

{memory.description}

- {memory.content} - {onSelect ? ( - - ) : null} -
- ); -} - -export function MemoryEditor({ - initial, - allowed = true, - error, - busy = false, - onSave, -}: { - initial?: Partial; - allowed?: boolean; - error?: string; - busy?: boolean; - onSave?: (input: UpsertMemoryInput) => void | Promise; -}) { - const [description, setDescription] = useState(initial?.description ?? ''); - const [content, setContent] = useState(initial?.content ?? ''); - if (!allowed) return ; - return ( -
{ - event.preventDefault(); - void onSave?.({ - description: description.trim(), - content: content.trim(), - }); - }} - > - {error ? ( -
- {error} -
- ) : null} - -