diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 734e9d6a..9a1cee52 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,6 +43,7 @@ jobs: outputs: docs_only: ${{ steps.detect.outputs.docs_only }} app_changed: ${{ steps.detect.outputs.app_changed }} + server_changed: ${{ steps.detect.outputs.server_changed }} steps: - uses: actions/checkout@v4 with: @@ -58,6 +59,7 @@ jobs: echo "Event is '$EVENT' (not pull_request) — running the full suite." echo "docs_only=false" >> "$GITHUB_OUTPUT" echo "app_changed=true" >> "$GITHUB_OUTPUT" + echo "server_changed=true" >> "$GITHUB_OUTPUT" exit 0 fi changed=$(git diff --name-only "$BASE_SHA...$HEAD_SHA") @@ -65,21 +67,24 @@ jobs: echo "$changed" docs_only=true app_changed=false + server_changed=false while IFS= read -r f; do [ -z "$f" ] && continue case "$f" in docs/*) ;; site/*) ;; app/*) app_changed=true ;; + server/*) docs_only=false; server_changed=true ;; llms.txt|llms-full.txt) ;; README.md|CHANGELOG.md|CONTRIBUTING.md|PRIVACY.md|ARCHITECTURE.md|RELEASING.md|LICENSE) ;; # NOTE: SKILL.md is intentionally not here — the suite tests it. *) docs_only=false ;; esac done <<< "$changed" - echo "Result: docs_only=$docs_only app_changed=$app_changed" + echo "Result: docs_only=$docs_only app_changed=$app_changed server_changed=$server_changed" echo "docs_only=$docs_only" >> "$GITHUB_OUTPUT" echo "app_changed=$app_changed" >> "$GITHUB_OUTPUT" + echo "server_changed=$server_changed" >> "$GITHUB_OUTPUT" bats: name: bats (${{ matrix.os }}) @@ -284,6 +289,112 @@ jobs: if: needs.changes.outputs.app_changed != 'false' run: cargo test --manifest-path src-tauri/Cargo.toml + # PostgreSQL-backed contract tests for the independent reference server. + # Like app-check, this always reports and skips heavy work only when the diff + # provably does not touch server/. + server-check: + name: remote server + needs: changes + if: ${{ !cancelled() }} + runs-on: ubuntu-latest + timeout-minutes: 10 + services: + postgres: + image: postgres:17-alpine + env: + POSTGRES_DB: agmsg_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres -d agmsg_test" + --health-interval 2s + --health-timeout 2s + --health-retries 20 + defaults: + run: + working-directory: server + steps: + - uses: actions/checkout@v4 + + - name: No server changes — skipping checks + if: needs.changes.outputs.server_changed == 'false' + run: echo "Diff does not touch server/ — reporting green without running server checks." + + - uses: actions/setup-node@v4 + if: needs.changes.outputs.server_changed != 'false' + with: + node-version: 22 + cache: npm + cache-dependency-path: server/package-lock.json + + - name: Install dependencies + if: needs.changes.outputs.server_changed != 'false' + run: npm ci + + - name: Typecheck + if: needs.changes.outputs.server_changed != 'false' + run: npm run typecheck + + - name: PostgreSQL integration tests + if: needs.changes.outputs.server_changed != 'false' + env: + TEST_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/agmsg_test + run: npm test + + - name: Build + if: needs.changes.outputs.server_changed != 'false' + run: npm run build + + - name: Compiled start smoke test + if: needs.changes.outputs.server_changed != 'false' + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/agmsg_test + AGMSG_SERVER_TOKEN: ci-compiled-start-token + LOG_LEVEL: silent + run: | + npm start > "$RUNNER_TEMP/agmsg-server.log" 2>&1 & + server_pid=$! + cleanup() { + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + } + trap cleanup EXIT + for _attempt in $(seq 1 20); do + if curl --fail --silent http://127.0.0.1:8787/v1/health >/dev/null; then + exit 0 + fi + sleep 1 + done + cat "$RUNNER_TEMP/agmsg-server.log" + exit 1 + + - name: Docker image and health smoke test + if: needs.changes.outputs.server_changed != 'false' + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/agmsg_test + AGMSG_SERVER_TOKEN: ci-docker-start-token + CONTAINER_NAME: agmsg-reference-ci-${{ github.run_id }} + run: | + docker build -t agmsg-reference-server:ci . + docker run --detach --name "$CONTAINER_NAME" --network host \ + --env DATABASE_URL --env AGMSG_SERVER_TOKEN \ + --env HOST=127.0.0.1 --env PORT=8788 --env LOG_LEVEL=silent \ + agmsg-reference-server:ci + cleanup() { + docker rm --force "$CONTAINER_NAME" >/dev/null 2>&1 || true + } + trap cleanup EXIT + for _attempt in $(seq 1 20); do + if curl --fail --silent http://127.0.0.1:8788/v1/health >/dev/null; then + exit 0 + fi + sleep 1 + done + docker logs "$CONTAINER_NAME" + exit 1 + # Windows leg for the app: the command layer's bash resolution and path # conversion are all behind cfg(windows) and had never run in CI — the very # 0.1.1→0.1.3 regressions (WSL bash.exe, backslash argv). This runs cargo test diff --git a/PRINCIPLES.md b/PRINCIPLES.md new file mode 100644 index 00000000..ccebff56 --- /dev/null +++ b/PRINCIPLES.md @@ -0,0 +1,63 @@ +# agmsg principles + +agmsg is an open-source messaging layer for AI agent teams. As the project +grows — including work that makes agmsg usable across machines and, in the +future, hosted services operated by the maintainers — these are the +commitments we hold ourselves to. They are design constraints, not +marketing. Changes to this document are made in the open, with reasoning. + +## 1. The core works on its own + +The agmsg core is open source and fully functional without any server, +account, or hosted service — today's local workflow is not a demo tier. +We will not move existing core functionality behind a hosted offering. + +## 2. Local-first is a design rule, not a feature + +An agent's hot path never waits on a network. Messages commit locally +first; anything remote synchronizes in the background and catches up after +being offline. Remote availability may delay sync — it must never corrupt +or block local work. + +## 3. The protocol is open + +The synchronization protocol is specified in the open, and a self-hostable +reference server is published as open source. Anything that can talk the +protocol is a first-class citizen: a hosted service run by us is one +provider among possible providers, and interoperability is not reserved +for it. + +## 4. Your data leaves with you + +Whatever stores your messages — local files or a remote server — you can +export all of your data, at any time, in an open format. Leaving must +always be a supported path, not a negotiation. + +## 5. Encryption is structural, not bolted on + +The remote protocol carries message contents in sealed envelopes that +servers store without parsing; the server-side schema has no plaintext +message fields to begin with. End-to-end encryption is implemented as a +first-class mode, and we are honest about its limits: it protects +contents, not traffic patterns. + +## 6. Commercial services sell operation, not the software + +The maintainers — and anyone else; the protocol is open — may run paid +services around agmsg. What such services sell is the work of running +things: servers, storage, uptime, organization-level management. The +software itself stays open, and any boundary between open code and a paid +service is drawn in the open. + +## 7. Community changes are judged by open-source value + +Contributions and design changes to the core are evaluated by what they do +for open-source users. Requirements that originate from a hosted or +commercial context must earn their place by having standalone value in the +open-source project, and are declined otherwise. + +--- + +*This document states our current commitments and how we intend to keep +working. It is versioned with the repository; if it ever needs to change, +the change and its reasoning will be public.* diff --git a/README.ja.md b/README.ja.md index 6b6076c7..e16ef23a 100644 --- a/README.ja.md +++ b/README.ja.md @@ -555,6 +555,10 @@ agmsgのプラグイン可能な単位は軸(axis)ごとにグループ化 agmsgでコピペの往復が省けたなら、GitHubスターが他の人にこのプロジェクトを見つけてもらう助けになる。 +## 原則 + +agmsgが成長していく中で自らに課す約束事 — ローカルファーストを設計原則として、同期プロトコルはオープンに、データはいつでも持ち出せる形で、暗号化は構造的に組み込む、など。[PRINCIPLES.md](PRINCIPLES.md)(英語)を参照。 + ## ライセンス MIT diff --git a/README.md b/README.md index ddc01103..9faf2d1e 100644 --- a/README.md +++ b/README.md @@ -591,6 +591,10 @@ See [Design & Architecture](docs/design.md) for developer documentation — iden If agmsg saves you copy-paste round-trips, a GitHub star helps other people find it. +## Principles + +The commitments agmsg holds itself to as it grows — local-first as a design rule, an open sync protocol, no lock-in on your data, structural encryption. See [PRINCIPLES.md](PRINCIPLES.md). + ## License MIT diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 00000000..1fc037f1 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,4 @@ +dist +node_modules +test +.env diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 00000000..deed335b --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.env diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 00000000..c07b24a4 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,18 @@ +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY tsconfig.json tsconfig.build.json ./ +COPY migrations ./migrations +COPY src ./src +RUN npm run build + +FROM node:22-alpine +ENV NODE_ENV=production +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev +COPY --from=build /app/dist ./dist +USER node +EXPOSE 8787 +CMD ["node", "dist/src/index.js"] diff --git a/server/README.md b/server/README.md new file mode 100644 index 00000000..a0ddd6ac --- /dev/null +++ b/server/README.md @@ -0,0 +1,72 @@ +# agmsg remote storage reference server + +This directory contains the thin, self-hosted PostgreSQL reference +implementation of the [HTTP API v1 contract](spec/v1.md). It is independent of +the root installer package and desktop app. + +The server currently implements the v1 plaintext envelope (`cipher: "none"`). +It stores envelope blobs opaquely and does not inspect sender, recipient, body, +or client creation time. Authentication is a deployment concern in the protocol; +this reference uses one required bearer token and still authorizes every +team-scoped operation against an immutable `Agmsg-Team-ID`. + +## Run with Compose + +Change the token and database password in `compose.yaml` before exposing the +service outside a local development machine, then run: + +```sh +docker compose up --build +``` + +`GET /v1/health` is available without credentials. The message, member, and +capability endpoints require these headers: + +```http +Authorization: Bearer +Agmsg-Protocol-Version: 1 +Agmsg-Team-ID: +``` + +## Run from source + +Node.js 22 and PostgreSQL 17 are the reference versions. + +```sh +npm install +export DATABASE_URL=postgresql://localhost/agmsg +export AGMSG_SERVER_TOKEN='replace-with-at-least-16-bytes' +npm run migrate +npm run provision -- example/team.json +npm run dev +``` + +The server also runs the idempotent migration at startup. Team creation and +roster mutation remain outside HTTP v1: the provisioning command atomically +applies the complete operator-owned roster manifest. IDs and retired names are +checked against permanent identity history before replacement. + +Retention is also an operator operation. It atomically creates permanent +idempotency tombstones, removes the covered delivery prefix, and advances the +team cursor floor while holding the same team-row lock as writers: + +```sh +npm run retain -- +``` + +## Verify + +Integration tests use an isolated, randomly named PostgreSQL schema. The test +only removes the schema it created and validates its generated name first. + +```sh +export TEST_DATABASE_URL=postgresql://localhost/agmsg_test +npm run typecheck +npm test +npm run build +``` + +The integration suite covers atomic batch rollback, complete input-order ack +mapping, transactional team sequence allocation, UUID conflict handling, +retention tombstones, cursor floors, capability snapshots, roster reads, and +strict JSON framing. diff --git a/server/compose.yaml b/server/compose.yaml new file mode 100644 index 00000000..4f647f43 --- /dev/null +++ b/server/compose.yaml @@ -0,0 +1,30 @@ +services: + postgres: + image: postgres:17-alpine + environment: + POSTGRES_DB: agmsg + POSTGRES_USER: agmsg + POSTGRES_PASSWORD: agmsg-local-only + healthcheck: + test: ["CMD-SHELL", "pg_isready -U agmsg -d agmsg"] + interval: 2s + timeout: 2s + retries: 20 + volumes: + - agmsg-postgres:/var/lib/postgresql/data + + server: + build: . + depends_on: + postgres: + condition: service_healthy + environment: + DATABASE_URL: postgresql://agmsg:agmsg-local-only@postgres:5432/agmsg + AGMSG_SERVER_TOKEN: change-this-development-token + HOST: 0.0.0.0 + PORT: 8787 + ports: + - "8787:8787" + +volumes: + agmsg-postgres: diff --git a/server/example/team.json b/server/example/team.json new file mode 100644 index 00000000..c467d24f --- /dev/null +++ b/server/example/team.json @@ -0,0 +1,17 @@ +{ + "team_id": "018f3f7e-0000-7000-8000-000000000001", + "team_name": "example-team", + "members": [ + { + "member_id": "018f3f7e-0000-7000-8000-000000000010", + "name": "worker-1", + "registrations": [ + { + "registration_id": "018f3f7e-0000-7000-8000-000000000011", + "installation_id": "018f3f7e-0000-7000-8000-000000000012", + "type": "codex" + } + ] + } + ] +} diff --git a/server/migrations/001_initial.sql b/server/migrations/001_initial.sql new file mode 100644 index 00000000..e501a4a6 --- /dev/null +++ b/server/migrations/001_initial.sql @@ -0,0 +1,89 @@ +CREATE TABLE IF NOT EXISTS server_metadata ( + singleton BOOLEAN PRIMARY KEY DEFAULT TRUE CHECK (singleton), + server_instance_id UUID NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp() +); + +CREATE TABLE IF NOT EXISTS teams ( + team_id UUID PRIMARY KEY, + team_name TEXT NOT NULL, + current_seq BIGINT NOT NULL DEFAULT 0 CHECK (current_seq >= 0), + min_available_seq BIGINT NOT NULL DEFAULT 0, + policy_revision BIGINT NOT NULL DEFAULT 0 CHECK (policy_revision >= 0), + accepted_envelope_versions INTEGER[] NOT NULL DEFAULT ARRAY[1], + write_allowed_ciphers TEXT[] NOT NULL DEFAULT ARRAY['none']::TEXT[], + max_blob_bytes INTEGER NOT NULL DEFAULT 1048576 + CHECK (max_blob_bytes BETWEEN 1 AND 1048576), + members_revision BIGINT NOT NULL DEFAULT 0 CHECK (members_revision >= 0), + CHECK (min_available_seq >= 0 AND min_available_seq <= current_seq) +); + +CREATE TABLE IF NOT EXISTS team_policy_history ( + team_id UUID NOT NULL REFERENCES teams(team_id) ON DELETE RESTRICT, + policy_revision BIGINT NOT NULL CHECK (policy_revision >= 0), + effective_from_seq BIGINT NOT NULL CHECK (effective_from_seq >= 1), + accepted_envelope_versions INTEGER[] NOT NULL, + write_allowed_ciphers TEXT[] NOT NULL, + PRIMARY KEY (team_id, policy_revision) +); + +CREATE INDEX IF NOT EXISTS team_policy_history_effective_idx + ON team_policy_history(team_id, effective_from_seq, policy_revision DESC); + +CREATE TABLE IF NOT EXISTS messages ( + team_id UUID NOT NULL REFERENCES teams(team_id) ON DELETE RESTRICT, + id UUID NOT NULL, + team_seq BIGINT NOT NULL CHECK (team_seq >= 1), + server_received_at TIMESTAMPTZ(6) NOT NULL DEFAULT clock_timestamp(), + envelope_v INTEGER NOT NULL, + cipher TEXT NOT NULL, + key_id TEXT, + blob TEXT NOT NULL, + envelope_digest BYTEA NOT NULL, + PRIMARY KEY (team_id, id), + UNIQUE (team_id, team_seq) +); + +CREATE TABLE IF NOT EXISTS message_tombstones ( + team_id UUID NOT NULL REFERENCES teams(team_id) ON DELETE RESTRICT, + id UUID NOT NULL, + original_team_seq BIGINT NOT NULL CHECK (original_team_seq >= 1), + envelope_digest BYTEA NOT NULL, + PRIMARY KEY (team_id, id) +); + +CREATE TABLE IF NOT EXISTS members ( + team_id UUID NOT NULL REFERENCES teams(team_id) ON DELETE RESTRICT, + member_id UUID NOT NULL, + name TEXT NOT NULL, + PRIMARY KEY (team_id, member_id), + UNIQUE (team_id, name) +); + +CREATE TABLE IF NOT EXISTS member_identity_history ( + team_id UUID NOT NULL REFERENCES teams(team_id) ON DELETE RESTRICT, + member_id UUID NOT NULL, + name TEXT NOT NULL, + PRIMARY KEY (team_id, name) +); + +CREATE INDEX IF NOT EXISTS member_identity_history_member_idx + ON member_identity_history(team_id, member_id); + +CREATE TABLE IF NOT EXISTS registrations ( + team_id UUID NOT NULL, + registration_id UUID NOT NULL, + member_id UUID NOT NULL, + installation_id UUID NOT NULL, + type TEXT NOT NULL, + PRIMARY KEY (team_id, registration_id), + FOREIGN KEY (team_id, member_id) + REFERENCES members(team_id, member_id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS registration_identity_history ( + team_id UUID NOT NULL REFERENCES teams(team_id) ON DELETE RESTRICT, + registration_id UUID NOT NULL, + member_id UUID NOT NULL, + PRIMARY KEY (team_id, registration_id) +); diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 00000000..bbb40ddc --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,2990 @@ +{ + "name": "@agmsg/reference-server", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@agmsg/reference-server", + "version": "0.1.0", + "dependencies": { + "fastify": "5.10.0", + "json-dup-key-validator": "1.0.3", + "pg": "8.22.0", + "uuid": "14.0.1", + "zod": "4.4.3" + }, + "devDependencies": { + "@types/node": "^22.20.1", + "@types/pg": "8.20.0", + "tsx": "4.23.1", + "typescript": "7.0.2", + "vitest": "4.1.10" + }, + "engines": { + "node": ">=22" + } + }, + "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, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "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, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^7.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "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": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "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", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "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/@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": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "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/@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" + }, + "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", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitest/expect": { + "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": "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": "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": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "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": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "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": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "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": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "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", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "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": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz", + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/backslash": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/backslash/-/backslash-0.2.2.tgz", + "integrity": "sha512-PKRYPE2LLTtNUYz1dszquxKSBs6XyLyJRHgFpY5rlq7y3DscDx239k5Gm+zenoY47OU4CApan1o0k2R8ptZC1Q==" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "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/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/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/fast-uri": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.1.tgz", + "integrity": "sha512-YPOs1zD5TG2+EZt+r88LwF6mclA7TPkpwMP7ZN3TO2HiHS8TXvq7QA/17iJsV9dubcLo/f8eEYqMBruyQV21hQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.10.0.tgz", + "integrity": "sha512-A9L0ziuWGQHgEEVgF3davQ9vbD93IuX+lo2IsxapQmu5b/Y/ynn9m9K5JHt9dvyJXOFc5iN0Zk5GHEOqnzhWjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^7.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-my-way": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", + "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/json-dup-key-validator": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json-dup-key-validator/-/json-dup-key-validator-1.0.3.tgz", + "integrity": "sha512-JvJcV01JSiO7LRz7DY1Fpzn4wX2rJ3dfNTiAfnlvLNdhhnm0Pgdvhi2SGpENrZn7eSg26Ps3TPhOcuD/a4STXQ==", + "license": "MIT", + "dependencies": { + "backslash": "^0.2.0" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "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/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "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", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "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": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@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-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "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/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vite": { + "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": { + "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" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "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", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "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.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "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": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@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": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 00000000..6f307a5c --- /dev/null +++ b/server/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agmsg/reference-server", + "version": "0.1.0", + "private": true, + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json && cp -R migrations dist/migrations", + "dev": "tsx watch src/index.ts", + "migrate": "tsx src/migrate.ts", + "provision": "tsx src/provision.ts", + "retain": "tsx src/retain.ts", + "start": "node dist/src/index.js", + "test": "vitest run", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "fastify": "5.10.0", + "json-dup-key-validator": "1.0.3", + "pg": "8.22.0", + "uuid": "14.0.1", + "zod": "4.4.3" + }, + "devDependencies": { + "@types/node": "^22.20.1", + "@types/pg": "8.20.0", + "tsx": "4.23.1", + "typescript": "7.0.2", + "vitest": "4.1.10" + } +} diff --git a/server/spec/v1.md b/server/spec/v1.md new file mode 100644 index 00000000..9c87f9cf --- /dev/null +++ b/server/spec/v1.md @@ -0,0 +1,765 @@ +# agmsg Remote Storage HTTP API v1 + +Status: draft for protocol review + +This document is the normative contract between agmsg storage drivers and the +self-hosted reference server. The words MUST, MUST NOT, SHOULD, and MAY are to +be interpreted as described by RFC 2119. + +## Scope and conventions + +V1 defines five endpoints: + +- `POST /v1/messages` +- `GET /v1/messages?after=` +- `GET /v1/members` +- `GET /v1/capabilities` +- `GET /v1/health` + +JSON is used for every request and response except requests without a body. +Request bodies MUST use `Content-Type: application/json` and UTF-8, MUST NOT +exceed 2 MiB as counted from streamed request bytes, and MUST NOT contain +duplicate object keys. V1 rejects any `Content-Encoding` other than `identity`. +JSON field names are +case-sensitive. Unknown request fields MUST be rejected with +`400 invalid-request`, so clients do not silently believe an unsupported field +was stored. Clients MUST ignore unknown response fields. + +Every team-scoped request MUST include: + +```http +Agmsg-Protocol-Version: 1 +Agmsg-Team-ID: +``` + +Every response, including errors, MUST include: + +```http +Agmsg-Protocol-Version: 1 +``` + +`GET /v1/health` does not require either request header because it is also the +version-discovery and orchestration probe. A missing, malformed, or unsupported +version on another endpoint returns `426 unsupported-protocol-version` and the +server's supported versions. The header version MUST equal the URL path version. +Selecting credentials and authenticating access to the team are deployment +concerns outside this protocol version; a server MUST NOT treat the +`Agmsg-Team-ID` header alone as authorization. Credentials MUST NOT be embedded +in that header or in URLs; deployments supply credentials separately (for +example, an `Authorization` header). + +Timestamps MUST use the exact UTC form `YYYY-MM-DDTHH:MM:SS.ffffffZ`, with six +fractional digits and no leap seconds. Sequence values in JSON and query +parameters are canonical decimal strings matching `0|[1-9][0-9]*` and MUST fit +in a signed 64-bit integer. They are strings so JavaScript clients never lose +PostgreSQL `BIGINT` precision. Team IDs and server instance IDs MUST be +canonical lowercase UUIDv7 values as specified by RFC 9562. A team ID is never +reused, including after deletion and recreation with the same display name. + +Agent-name strings are first normalized to NFC and then MUST contain 1–128 +Unicode scalar values. They MUST NOT start with `-`, equal `.` or `..`, contain +any of `.`, `/`, `\`, `"`, `[`, or `]`, or contain U+0000–U+001F or U+007F. +These rules pin remote member projection to the local agmsg agent-name safety +contract while retaining non-ASCII names. + +Message `id` is a client-generated opaque 128-bit UUID value represented as a +canonical lowercase UUIDv4 (122 random payload bits plus the required UUID +version and variant bits). It MUST be generated with a cryptographically secure +random number generator, be unique within the team stream, be generated once +with the envelope, and be durably stored before POST. A timestamp-bearing UUID, +including UUIDv7, MUST NOT be used as the wire ID. Local driver IDs remain local +and continue to follow the local driver's UUIDv7 contract; clients persist the +local-ID/wire-ID mapping together with the immutable canonical envelope in +reconciliation state and MUST NOT regenerate a wire ID during retry. A pulled +wire ID with no mapping receives a new local ID under the local driver contract, +and that mapping is made durable as part of import. A pulled wire ID that +already has a mapping follows the echo-reconciliation rules under Pull state +layers and MUST NOT receive another local ID. + +Each database deployment has an immutable, canonical lowercase UUIDv7 +`server_instance_id`, created once and persisted with the database. Restoring or +moving the same database retains it; initializing a new database creates a new +one. Reverse-proxy aliases for one deployment therefore share the ID, while a +replacement database at the same URL does not. + +Clients MUST key cursors by `(server_instance_id, team_id, protocol version)` +and MUST stop if a response ID differs from its stored binding. Before the first +team request, a client discovers `server_instance_id` from health. Clients MUST +NOT automatically follow redirects or forward authorization/team headers to +another origin. + +## Common response fields + +Successful team-scoped responses include: + +```json +{ + "protocol_version": 1, + "server_instance_id": "018f3f7e-0000-7000-8000-000000000000", + "team_id": "018f3f7e-0000-7000-8000-000000000001", + "team_name": "example-team", + "min_available_seq": "0" +} +``` + +`team_id` is the immutable stream identity. `team_name` is a mutable display +label only. It is first normalized to NFC and then MUST contain 1–128 Unicode +scalar values; comparison is case-sensitive. It MUST NOT identify cursor state. + +`min_available_seq` is the smallest valid cursor for the team. It is normally +zero. If retention removes all messages through sequence `N`, it becomes `N`. +A client cursor smaller than this floor cannot be incrementally repaired. + +Errors have one shape: + +```json +{ + "protocol_version": 1, + "error": { + "code": "invalid-request", + "message": "human-readable summary", + "details": {} + } +} +``` + +Clients MUST branch on `error.code`, not on `message`. + +Errors produced before database/team resolution (`400`, `401`, malformed +version `426`) omit `server_instance_id` and `team_id`. Once the database is +resolved, every application error includes `server_instance_id`; once the team +is resolved, it also includes `team_id`. Thus `403`, `409`, `410`, `429`, and +team-scoped `503` include both IDs. `404 team-not-found` includes +`server_instance_id` and the requested `team_id`. Clients apply the same ID +mismatch stop rule to success and error responses. + +## Message representation and encryption envelope + +The client-supplied immutable message is: + +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "envelope": { + "v": 1, + "cipher": "none", + "key_id": null, + "blob": "eyJib2R5IjoiUnVuIHRoZSB0ZXN0IHN1aXRlIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDctMjBUMDY6MzA6MDAuMDAwMDAwWiIsImZyb21fYWdlbnQiOiJsZWFkZXIiLCJ0b19hZ2VudCI6Indvcmtlci0xIn0=" + } +} +``` + +`id` and all four envelope fields are required. `v` is the envelope format +version and is the integer `1`. `cipher` is `none` for the v1 +driver; future drivers may use additional lowercase scheme identifiers matching +`[a-z0-9][a-z0-9._-]{0,63}` without changing this HTTP or database contract. +`key_id` MUST be either JSON null or a JSON string already normalized to NFC. +A non-null value is 1–256 UTF-8 bytes, contains no U+0000–U+001F or U+007F, and +is compared by exact UTF-8 bytes without case folding. It MUST be null for +`none`; encrypted schemes use a non-null scheme-defined key/epoch identifier so +readers never trial-decrypt against unbounded keys. +`blob` is 1–1,398,104 ASCII characters of canonical padded RFC 4648 base64 +(representing at most 1 MiB). The server MAY base64-decode solely to verify +canonical padding/size and compute the idempotency digest. It MUST treat the +resulting bytes as opaque and MUST NOT persist or log the decoded form. + +The server MUST treat `blob` contents as opaque. It MUST NOT decode, parse, +index, filter, log, or validate plaintext inside `blob`. The only +application-message fields visible to the server are team authorization +context, `id`, `server_seq`, and server-assigned `server_received_at`. Envelope +admission metadata (`v`, `cipher`, `key_id`, and blob byte length) is also +necessarily visible for capability and policy enforcement, but is not a +routing or search surface. Client creation time, sender, recipient, and body +live inside the blob; recipient filtering occurs locally after team-stream +synchronization. + +For `cipher: "none"`, decoded bytes MUST be RFC 8785 JSON Canonicalization +Scheme (JCS) encoding of exactly this object (no unknown or duplicate keys): + +```json +{"body":"Run the test suite","created_at":"2026-07-20T06:30:00.000000Z","from_agent":"leader","to_agent":"worker-1"} +``` + +All fields are required strings. `created_at` follows the canonical timestamp +rule. `from_agent` and `to_agent` follow the agent-name rules above. `body` is +non-empty and at most 1,000,000 UTF-8 bytes before JSON encoding. Clients +produce and validate this plaintext format, but the server remains unaware of +it. The complete JCS-encoded plaintext, after JSON escaping, MUST fit within +the smaller of the protocol's 1 MiB decoded-blob limit and the authenticated +capability response's `max_blob_bytes`. The body limit alone is not sufficient +admission validation. + +Before the first POST, a client MUST durably store the exact `id`, `v`, +`cipher`, `key_id`, and `blob`. Every retry, crash recovery, and compaction replay +MUST reuse those exact values; it MUST NOT reserialize, re-encode, or re-encrypt +the same ID. This remains required for randomized future encryption. + +V1 clients MUST emit `cipher: "none"`; a server may recognize additional cipher +profiles while continuing to store their blobs opaquely. A v1 client receiving +an unknown cipher MUST +durably quarantine the unchanged envelope, surface an unsupported-cipher state, +and may advance the team cursor only after quarantine is durable. It MUST NOT +discard or interpret the blob. Future ciphers require separately pinned scheme +profiles, capability handling, key lifecycle, AEAD associated-data rules, and +padding policy. Those contracts are reserved, not defined by v1. The envelope +boundary keeps HTTP message columns/endpoints stable; client crypto contracts +are not claimed to remain unchanged. + +Any future authenticated-encryption profile MUST bind this wire `id` as the +message-UUID component of its AEAD associated data. A local driver ID MUST NOT +be used for that purpose because it differs across replicas. + +UUID conflict equality is keyed by the wire `id` and compares exact canonical +envelope bytes: RFC 8785 JCS encoding of the object containing `v`, `cipher`, +`key_id`, and `blob`. JSON whitespace and input key order are irrelevant, but +all envelope values are byte-stable. Server-assigned `server_received_at` is not +part of conflict equality. + +A server-returned message adds its canonical team sequence: + +```json +{ + "server_seq": "42", + "id": "550e8400-e29b-41d4-a716-446655440000", + "server_received_at": "2026-07-20T06:30:01.000000Z", + "envelope": { + "v": 1, + "cipher": "none", + "key_id": null, + "blob": "eyJib2R5IjoiUnVuIHRoZSB0ZXN0IHN1aXRlIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDctMjBUMDY6MzA6MDAuMDAwMDAwWiIsImZyb21fYWdlbnQiOiJsZWFkZXIiLCJ0b19hZ2VudCI6Indvcmtlci0xIn0=" + } +} +``` + +`server_seq` and `team_seq` refer to the same value. HTTP schemas use +`server_seq`; the database and ordering rules call it `team_seq`. +`server_received_at` is assigned once by the server when a new ID is inserted, +uses the canonical timestamp format, and never changes on duplicate replay. It +is ordering/diagnostic metadata only; sequence remains authoritative. + +## `GET /v1/capabilities` + +This authenticated, team-scoped endpoint advertises the current write policy. +It is deliberately separate from unauthenticated health and says nothing about +which ciphers a particular client can read. + +Independently of server policy, every client sync configuration MUST durably +pin an append-only local security history to its `(server_instance_id, team_id)` +binding. Each entry contains canonical decimal `local_security_revision` and +`effective_from_seq` plus `minimum_security_mode`. V1 defines +`plaintext-allowed` and `e2ee-required`; the latter excludes `none`. The initial +entry has revision `0`, is effective from sequence `1`, and requires an explicit +local/operator mode choice. A server response MUST NOT silently create, lower, +or replace this history. An operator-approved replacement binding MUST +explicitly establish a new history and MUST NOT silently lower the prior +security posture. + +The history is part of durable sync configuration, not ephemeral device state. +A new device for an existing binding MUST import the established history from +trusted configuration or require explicit operator provisioning; it MUST NOT +reconstruct history from only the server's current policy or silently apply the +current local mode back to sequence `1`. + +For a message at sequence `S`, the effective local minimum is the entry with the +greatest local revision whose `effective_from_seq <= S`. Same-boundary changes +collapse to the greatest revision. Clients MUST bound effective local history +to 4096 entries just like server policy history and MUST reject a further +distinct-boundary change atomically. For new messages, the effective write set +is the intersection of the server's `write_allowed_ciphers`, locally supported +ciphers, and ciphers satisfying the effective local minimum. An empty +intersection is a terminal configuration state, not a reason to downgrade. + +```json +{ + "protocol_version": 1, + "server_instance_id": "018f3f7e-0000-7000-8000-000000000000", + "team_id": "018f3f7e-0000-7000-8000-000000000001", + "team_name": "example-team", + "min_available_seq": "0", + "current_seq": "0", + "next_sequence_boundary": "1", + "accepted_envelope_versions": [1], + "write_allowed_ciphers": ["none"], + "policy_revision": "0", + "effective_from_seq": "1", + "max_blob_bytes": "1048576", + "policy_history": [ + { + "policy_revision": "0", + "effective_from_seq": "1", + "accepted_envelope_versions": [1], + "write_allowed_ciphers": ["none"] + } + ] +} +``` + +`policy_revision` is a server-assigned canonical decimal string, initially `0`, +incremented atomically whenever policy changes. `effective_from_seq` is the +first team sequence governed by this policy revision. Sequences begin at `1`, +so a newly provisioned team's revision `0` is effective from `1`. These values +let readers distinguish permitted legacy plaintext from a post-policy +downgrade. `policy_history` is the complete, ascending effective history for the +immutable team ID, including the current revision. If multiple revisions share +one `effective_from_seq`, only the greatest revision is effective and the server +MUST collapse superseded entries in this response. At most 4096 effective +history entries may exist for a team; an administrative policy mutation that +would create another distinct boundary MUST fail atomically. The server MUST +retain and return this bounded history for the team's lifetime so clients with +durable older quarantine can evaluate the policy effective at any sequence. +Its final entry MUST exactly match the response's current `policy_revision`, +`effective_from_seq`, `accepted_envelope_versions`, and +`write_allowed_ciphers` fields. +For a message at sequence `S`, the effective policy is the history entry with +the greatest `policy_revision` among entries whose `effective_from_seq <= S`. +Multiple policy changes before the next message may therefore share one +`effective_from_seq` boundary. +`max_blob_bytes` is decoded opaque-byte size and MUST NOT exceed the protocol +maximum. + +`current_seq` is the team's current committed sequence at this snapshot. +`next_sequence_boundary` is `current_seq + 1`, or null when `current_seq` is the +signed BIGINT maximum. Both non-null values use the canonical sequence-string +format. A normal local security-mode change MUST fetch a fresh authenticated +capability snapshot, verify its binding, and durably append the next local +revision with `effective_from_seq = next_sequence_boundary`. It MUST fail if +that boundary is null. This makes plaintext-to-E2EE transitions prospective: +valid earlier plaintext keeps its earlier local policy. A request to hide or +reclassify earlier plaintext is a separate explicit retroactive local action; +it MUST NOT rewrite the normal effective history. + +Capability reads and POST policy enforcement MUST use the same authoritative +team policy state. Servers only advertise write admission. Client read/decrypt +capability is local state and MUST NOT be inferred from this response. +The sequence boundary, current fields, and complete `policy_history` MUST be +read from one database transaction and snapshot. Capability responses MUST send +`Cache-Control: no-store`; clients MUST NOT use a cached response to choose a +local effective boundary. +Policy mutation locks the team row, increments `policy_revision`, and records +`effective_from_seq = current_seq + 1` atomically. A policy change whose next +revision or `effective_from_seq` would exceed signed BIGINT maximum MUST fail +atomically rather than wrap. + +Before creating or POSTing a new envelope, a client MUST fetch this +authenticated response, verify its server/team binding, and preflight the +effective write set for `next_sequence_boundary`. A v1 client supports only +`none`; if the server disallows `none` or the effective local mode is +`e2ee-required`, it MUST stop without generating or POSTing a plaintext +envelope. A later POST policy race may still return `403`, which the client +handles as specified below. + +## `POST /v1/messages` + +Stores one atomic batch. The request body is: + +```json +{ + "messages": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "envelope": { + "v": 1, + "cipher": "none", + "key_id": null, + "blob": "eyJib2R5IjoiUnVuIHRoZSB0ZXN0IHN1aXRlIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDctMjBUMDY6MzA6MDAuMDAwMDAwWiIsImZyb21fYWdlbnQiOiJsZWFkZXIiLCJ0b19hZ2VudCI6Indvcmtlci0xIn0=" + } + } + ] +} +``` + +`messages` MUST contain between 1 and 1000 elements. The server validates the +whole request before committing any element. + +Inside the single transaction, processing order is normative: + +1. Authenticate/authorize `team_id`, lock the team row, and look up every input + ID (including permanent tombstones). +2. For an existing ID, compare exact canonical envelope bytes. An equal replay + receives its original duplicate ack regardless of current cipher policy; an + unequal replay makes the whole batch `409`. +3. For new IDs only, reject an envelope version/cipher the server cannot store + with `422 unsupported-cipher`. If the cipher is recognized but disallowed by + current team policy, reject with `403 cipher-policy-violation`. +4. Only after every element passes those steps may the server allocate sequences + and insert new IDs. + +Both cipher errors include `write_allowed_ciphers`, `policy_revision`, and the +offending `id`, `v`, and `cipher` in `error.details`. An +`unsupported-cipher` error also includes `accepted_envelope_versions`. This +ordering preserves idempotent replay of legacy ciphertext after policy changes +without allowing new downgrade writes. + +Success returns `200` for both new and replayed batches: + +```json +{ + "protocol_version": 1, + "server_instance_id": "018f3f7e-0000-7000-8000-000000000000", + "team_id": "018f3f7e-0000-7000-8000-000000000001", + "team_name": "example-team", + "min_available_seq": "0", + "policy_revision": "0", + "acks": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "server_seq": "42", + "disposition": "stored" + } + ] +} +``` + +The `acks` array MUST have the same length and order as the input array. Every +input element maps to its canonical `server_seq`; no element may be omitted. +`disposition` is `stored` if this transaction first inserted the UUID and +`duplicate` if an identical UUID/payload already existed. Repeated identical +UUIDs within one request receive the same sequence; only the first occurrence +can be `stored`. + +The entire batch is one database transaction. If any UUID already exists for +the team with a different immutable payload, or the same batch repeats a UUID +with differing payloads, the server MUST return +`409 message-uuid-conflict` and roll back every insertion and sequence update: + +```json +{ + "protocol_version": 1, + "server_instance_id": "018f3f7e-0000-7000-8000-000000000000", + "team_id": "018f3f7e-0000-7000-8000-000000000001", + "error": { + "code": "message-uuid-conflict", + "message": "message id already exists with a different payload", + "details": { + "id": "550e8400-e29b-41d4-a716-446655440000" + } + } +} +``` + +An implementation using `ON CONFLICT DO NOTHING RETURNING` MUST query the +conflicting UUIDs again inside the same transaction and fill the missing ack +mappings. Returning only rows produced by `RETURNING` violates this contract. + +## `GET /v1/messages?after=` + +Returns messages for the team in ascending `server_seq` order, strictly after +the supplied cursor. `after` is required and MUST be a canonical decimal +sequence string as defined above. +The optional `limit` is an integer from 1 through 1000 and defaults to 100. + +```http +GET /v1/messages?after=40&limit=100 +``` + +Success returns `200`: + +```json +{ + "protocol_version": 1, + "server_instance_id": "018f3f7e-0000-7000-8000-000000000000", + "team_id": "018f3f7e-0000-7000-8000-000000000001", + "team_name": "example-team", + "min_available_seq": "0", + "messages": [ + { + "server_seq": "41", + "id": "550e8400-e29b-41d4-a716-446655440000", + "server_received_at": "2026-07-20T06:30:01.000000Z", + "envelope": { + "v": 1, + "cipher": "none", + "key_id": null, + "blob": "eyJib2R5IjoiUnVuIHRoZSB0ZXN0IHN1aXRlIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDctMjBUMDY6MzA6MDAuMDAwMDAwWiIsImZyb21fYWdlbnQiOiJsZWFkZXIiLCJ0b19hZ2VudCI6Indvcmtlci0xIn0=" + } + } + ], + "next_after": "41", + "has_more": false +} +``` + +`next_after` is the last returned sequence, or the supplied `after` when the +page is empty. `has_more` states whether a later sequence was visible to the +same database snapshot but excluded by `limit`. The floor check, message page, +and `has_more` probe MUST use one database transaction and snapshot. + +If `after < min_available_seq`, the server returns `410 resync-required` and no +messages: + +```json +{ + "protocol_version": 1, + "server_instance_id": "018f3f7e-0000-7000-8000-000000000000", + "team_id": "018f3f7e-0000-7000-8000-000000000001", + "team_name": "example-team", + "min_available_seq": "100", + "error": { + "code": "resync-required", + "message": "cursor predates retained history", + "details": { + "after": "42", + "min_available_seq": "100" + } + } +} +``` + +The client MUST enter a terminal `resync-required` state, stop incremental +sync, and surface operator action. It MUST NOT reset the cursor or discard local +state automatically. V1 provides no full-snapshot operation; recovery is a +manual or driver-specific operation outside this protocol. + +### Pull state layers + +Clients MUST keep three independent progress layers: + +1. **Transport:** durably quarantine every envelope together with wire ID, + `server_seq`, server policy evaluation, and local security evaluation before + advancing the transport cursor. A crash must not lose an envelope whose + sequence was acknowledged locally. +2. **Decrypt/import:** process quarantined envelopes into states such as + `pending_key`, `unsupported_cipher`, `authentication_failed`, or `malformed`. + Key/capability changes may reprocess this layer without rewinding transport. +3. **Read:** user/agent read state advances independently of both transport and + decrypt/import state. + +Readers use capability policy history (`policy_revision` and +`effective_from_seq`) to distinguish allowed legacy `none` envelopes from a +post-policy downgrade attempt. They MUST also apply the durable local +security-history entry effective at the message's `server_seq`. An envelope +that violates the effective server policy or effective local minimum at its +sequence enters durable `policy_violation` with the reason recorded. It MUST +NOT be imported or exposed as readable, although the transport cursor may +advance after that quarantine record is durable. + +Pull echo reconciliation is normative. After durable quarantine, the client +looks up the wire ID in reconciliation state: + +- If a mapping exists and its canonical immutable envelope matches, the client + MUST NOT create a local ID or event. It durably records `server_seq` and remote + observation/confirmation on the already-mapped local record. +- If a mapping exists but points to a different immutable envelope, the client + enters durable `corrupt_state`, MUST NOT import or read the message, and MUST + surface operator action. +- Only if no mapping exists may the client allocate one local ID, durably store + the local-ID/wire-ID/envelope mapping, and import the message once. + +The transport cursor may advance only after the applicable reconciliation or +import is durable, or after a durable blocking quarantine state such as +`pending_key`, `unsupported_cipher`, `authentication_failed`, `malformed`, +`policy_violation`, or `corrupt_state` has been recorded. + +Because routing fields are opaque, the server cannot provide recipient routing, +search, or per-recipient delivery. Clients download the team stream and project +recipients locally; wake notification is team-wide. E2EE protects blob contents, +not metadata: team relationship, random wire ID, server receipt time, envelope +version, cipher identifier, key identifier, blob size, traffic frequency, and +sequence remain visible. Random UUIDv4 removes avoidable embedded generation +time but does not make metadata private. + +## `GET /v1/members` + +Returns the current team roster: + +```json +{ + "protocol_version": 1, + "server_instance_id": "018f3f7e-0000-7000-8000-000000000000", + "team_id": "018f3f7e-0000-7000-8000-000000000001", + "team_name": "example-team", + "min_available_seq": "0", + "members_revision": "7", + "members": [ + { + "member_id": "018f3f7e-0000-7000-8000-000000000010", + "name": "worker-1", + "registrations": [ + { + "registration_id": "018f3f7e-0000-7000-8000-000000000011", + "installation_id": "018f3f7e-0000-7000-8000-000000000012", + "type": "codex" + } + ] + } + ] +} +``` + +Members are sorted by `member_id`; registrations are sorted by +`registration_id`. All IDs are immutable UUIDv7 values. `name` follows the +agent-name rules above. `type` is a non-empty driver type string, and an agent +with no registrations has an empty array. `installation_id` identifies the +installation that owns a registration. Machine-local project paths MUST NOT be +uploaded or returned. + +V1 HTTP is read-only for members. The authoritative source is an operator +provisioning document applied by the reference server's local admin CLI. That +document contains `team_id`, `team_name`, and the complete `members` array in +the response schema above; input MUST NOT contain `members_revision`. Applying +it is an atomic replace for that team: +unchanged IDs update mutable fields, new IDs are created, omitted IDs are +removed, and `members_revision` increments transactionally. The server MUST NOT +infer members from message senders or recipients, and MUST NOT require direct +database edits. Other implementations MAY use another administrative control +plane, but MUST provide the same ID/lifecycle semantics and GET representation. +`members_revision` is a server-assigned signed-BIGINT canonical decimal string, +initially `0`. Provisioning at its maximum MUST fail atomically rather than wrap. + +Within a team, member IDs, normalized member names, and registration IDs MUST be +unique. A registration belongs to exactly one member. Reusing one +`installation_id` across multiple registrations is allowed. Provisioning MUST +reject the entire document on a duplicate or ownership conflict. Renaming a +member retains its `member_id`; removing a member or registration permanently +retires its ID, which MUST NOT later identify a different object. A normalized +member name, once assigned, MUST NOT later be assigned to a different +`member_id`; this prevents historical opaque sender/recipient names from being +projected onto a new identity. + +## `GET /v1/health` + +This endpoint is a readiness and version-discovery probe, not a process +liveness signal. Orchestrators MUST NOT use its `503` response alone as a +reason to restart the process during a database outage. + +A ready server returns `200` after a successful database probe and includes +the v1 response header: + +```http +Agmsg-Protocol-Version: 1 +``` + +```json +{ + "status": "ok", + "server_instance_id": "018f3f7e-0000-7000-8000-000000000000", + "protocol": { + "supported_versions": [1] + }, + "database": "ok" +} +``` + +If the process is serving HTTP but its database probe fails, it returns `503` +with `status: "unavailable"`, the same protocol advertisement, and +`database: "unavailable"`. It omits `server_instance_id` if that persisted value +could not be read, and clients MUST NOT create or change a cursor binding from +an unavailable response. Health responses MUST NOT expose credentials, +connection strings, or raw database errors. + +## Protocol negotiation errors + +Unsupported or absent versions on team-scoped endpoints return `426`: + +```json +{ + "protocol_version": 1, + "error": { + "code": "unsupported-protocol-version", + "message": "a supported Agmsg-Protocol-Version header is required", + "details": { + "requested_version": 2, + "supported_versions": [1] + } + } +} +``` + +The response header identifies the version that encoded the response envelope, +so `/v1` responses always use `Agmsg-Protocol-Version: 1`, even when the server +also supports newer versions. Supported alternatives are advertised only in +`details.supported_versions`. Clients MUST NOT silently downgrade a write. + +## Error and retry semantics + +All application-generated non-2xx responses use the common error envelope, +except the unauthenticated readiness shape from `GET /v1/health`. + +| HTTP | Error code | Meaning | Client action | +| --- | --- | --- | --- | +| 400 | `invalid-request` | Malformed JSON, header, team ID, cursor, or schema | Do not retry unchanged | +| 401 | `unauthenticated` | Credentials absent or invalid | Stop and refresh credentials | +| 403 | `forbidden` | Credentials cannot access this team | Stop | +| 403 | `cipher-policy-violation` | Recognized cipher is not currently write-allowed | Refresh capabilities; do not retry unchanged | +| 404 | `team-not-found` | Immutable team ID is not provisioned | Stop; never create implicitly | +| 409 | `message-uuid-conflict` | UUID payload differs | Stop and surface corruption/conflict | +| 410 | `resync-required` | Cursor is below retention floor | Enter terminal resync state | +| 413 | `request-too-large` | Body exceeds 2 MiB | Split/reduce while preserving exact durable IDs/envelopes | +| 422 | `unsupported-cipher` | Envelope version/cipher is not supported by this server | Stop or upgrade/reconfigure | +| 426 | `unsupported-protocol-version` | Header absent or differs from path/supported versions | Stop or explicitly reconfigure | +| 429 | `rate-limited` | Temporary admission limit | Retry with backoff and `Retry-After` | +| 500 | `internal-error` | Temporary unclassified server failure | Retry exact request with backoff | +| 502 | `unavailable` | Temporary upstream failure | Retry exact request with backoff | +| 503 | `unavailable` | Temporary server/database failure | Retry with backoff | +| 504 | `unavailable` | Temporary upstream timeout | Retry exact request with backoff | +| 507 | `sequence-exhausted` | Team sequence reached signed BIGINT maximum | Stop writes; reads remain available | + +A POST timeout, transport loss, or `500`, `502`, `503`, or `504` has unknown +outcome. The client MUST retry the exact durable ID/envelope batch and rely on +complete ack mapping; it MUST NOT mint replacement UUIDs. `409`, `422`, and +`507` are definitive and non-retryable. Servers SHOULD include +`Retry-After` on `429` and MAY include it on `503`. +An HTTP intermediary may generate `502`, `503`, or `504` without the protocol +error envelope; clients still apply the status-based retry rule and MUST verify +all binding IDs on the eventual protocol response. + +## Team sequence allocation and visibility + +V1 MUST NOT use a global sequence or a PostgreSQL `BIGSERIAL`/sequence object. +PostgreSQL sequences are non-transactional and can expose gaps or ordering that +does not match commit visibility. + +Each team has one row containing `current_seq` and `min_available_seq`. A write +transaction performs the following steps: + +1. Verify that operator provisioning created the immutable team row; otherwise + return `404 team-not-found` without creating it from an untrusted header. +2. Lock that row with `SELECT ... FOR UPDATE`. +3. Load every UUID in the batch, reject conflicts, enforce new-ID policy, and + count distinct new IDs as `K` (duplicate occurrences count once). +4. Before allocation, if `K > 9223372036854775807 - current_seq`, reject the + whole batch with `507 sequence-exhausted`. +5. In input first-occurrence order, increment the locked row's `current_seq` + once for each new UUID and insert that message with the resulting value. +6. Select all UUIDs again inside the transaction and build the complete, + input-ordered ack mapping. +7. Commit the messages, mappings, and updated `current_seq` atomically. + +Transactions for one team serialize on its team row. A later writer cannot +allocate a sequence until the prior holder commits or rolls back, so committed +`team_seq` order is the visibility order for that team. Transactions for +different teams do not share this lock and MAY proceed concurrently. A rollback +MUST leave neither messages nor consumed team sequence values. + +The preflight covers both an already-exhausted counter and a batch crossing the +boundary. The server MUST NOT wrap, become negative, or rely on a database +overflow error. Duplicate UUID lookups and existing reads remain available. + +Retention changes to `min_available_seq` MUST lock the same team row and commit +the new floor atomically with deletion/archival of the covered delivery rows. +The server MUST retain, for the lifetime of the immutable team ID, a dedupe +tombstone containing `(team_id, id, envelope_digest, original team_seq)`. +`envelope_digest` is SHA-256 over: ASCII `agmsg-envelope-v1` followed by a zero +byte; envelope `v` as unsigned 32-bit big-endian; unsigned 32-bit big-endian +length plus UTF-8 bytes of `cipher`; one byte `0` for null `key_id`, otherwise +byte `1` then its unsigned 32-bit big-endian byte length plus exact UTF-8 bytes; +and unsigned 32-bit big-endian length plus the base64-decoded opaque blob bytes. +This exact length-prefixed preimage is the canonical immutable payload digest. +Decoded bytes used for hashing are not persisted or logged. + +Retention MUST NOT permit a replayed UUID to acquire a new +sequence. An identical replay returns `duplicate` with its original sequence; +a differing replay remains `409`, even when its delivery row is below the pull +floor. V1 intentionally retains only permanent digest/idempotency metadata, not +the envelope/body, and advertises no finite replay window. SHA-256 collision +risk is accepted as the protocol's idempotency tradeoff. + +## Reference server technology (non-normative) + +The reference implementation lives in the repository's independent `server/` +package rather than the root installer package or Tauri app. The proposed stack +is TypeScript on Node.js 22 LTS with Fastify, `pg`, Zod, and Vitest. PostgreSQL +integration tests run against a real PostgreSQL service. A Dockerfile and +Compose example support self-hosting. These choices are implementation details; +clients are pinned to this protocol document, not to the reference server's +language or framework. diff --git a/server/src/app.ts b/server/src/app.ts new file mode 100644 index 00000000..a46ec0fd --- /dev/null +++ b/server/src/app.ts @@ -0,0 +1,183 @@ +import { timingSafeEqual } from "node:crypto"; +import Fastify, { + type FastifyInstance, + type FastifyRequest, +} from "fastify"; +import * as duplicateKeyJson from "json-dup-key-validator"; +import type { Pool } from "pg"; +import { ZodError, z } from "zod"; +import type { Config } from "./config.js"; +import { errorBody, ProtocolError } from "./errors.js"; +import { + MAX_REQUEST_BYTES, + messagesQuerySchema, + parseSequence, + postMessagesSchema, + uuidV7Schema, +} from "./protocol.js"; +import { + getCapabilities, + getMembers, + getMessages, + health, + postMessages, +} from "./storage.js"; + +const emptyQuerySchema = z.object({}).strict(); + +function tokenMatches(expected: string, actual: string): boolean { + const left = Buffer.from(expected); + const right = Buffer.from(actual); + return left.length === right.length && timingSafeEqual(left, right); +} + +function scopedTeamId(request: FastifyRequest, token: string): string { + const version = request.headers["agmsg-protocol-version"]; + if (version !== "1") { + throw new ProtocolError( + 426, + "unsupported-protocol-version", + "Agmsg-Protocol-Version must match /v1", + { requested_version: version ?? null, supported_versions: [1] }, + ); + } + const authorization = request.headers.authorization; + if ( + typeof authorization !== "string" || + !authorization.startsWith("Bearer ") || + !tokenMatches(token, authorization.slice("Bearer ".length)) + ) { + throw new ProtocolError(401, "unauthenticated", "valid credentials are required"); + } + const parsed = uuidV7Schema.safeParse(request.headers["agmsg-team-id"]); + if (!parsed.success) { + throw new ProtocolError(400, "invalid-request", "Agmsg-Team-ID is invalid"); + } + return parsed.data; +} + +export function createApp(pool: Pool, config: Config): FastifyInstance { + const app = Fastify({ + logger: config.logLevel === "silent" ? false : { level: config.logLevel }, + bodyLimit: MAX_REQUEST_BYTES, + }); + + app.removeContentTypeParser("application/json"); + app.addContentTypeParser( + "application/json", + { parseAs: "buffer" }, + (_request, body, done) => { + try { + const bytes = typeof body === "string" ? Buffer.from(body) : body; + const source = new TextDecoder("utf-8", { fatal: true }).decode(bytes); + done(null, duplicateKeyJson.parse(source, false)); + } catch (error) { + const parsingError = error instanceof Error ? error : new Error("invalid JSON"); + Object.assign(parsingError, { statusCode: 400 }); + done(parsingError, undefined); + } + }, + ); + + app.addHook("onRequest", async (request) => { + const encoding = request.headers["content-encoding"]; + if (encoding !== undefined && encoding !== "identity") { + throw new ProtocolError( + 400, + "invalid-request", + "Content-Encoding must be identity", + ); + } + }); + + app.addHook("onSend", async (_request, reply, payload) => { + reply.header("Agmsg-Protocol-Version", "1"); + return payload; + }); + + app.setErrorHandler((error, _request, reply) => { + if (error instanceof ProtocolError) { + void reply.status(error.statusCode).send(errorBody(error)); + return; + } + if ( + error instanceof ZodError || + error instanceof SyntaxError || + statusCode(error) === 400 || + statusCode(error) === 415 + ) { + const protocolError = new ProtocolError( + 400, + "invalid-request", + "request body, query, or JSON framing is invalid", + ); + void reply.status(400).send(errorBody(protocolError)); + return; + } + if (statusCode(error) === 413) { + const protocolError = new ProtocolError( + 413, + "request-too-large", + "request body exceeds 2 MiB", + ); + void reply.status(413).send(errorBody(protocolError)); + return; + } + requestLog(reply, error); + const protocolError = new ProtocolError( + 500, + "internal-error", + "an internal server error occurred", + ); + void reply.status(500).send(errorBody(protocolError)); + }); + + app.get("/v1/health", async (_request, reply) => { + try { + return await health(pool); + } catch { + return reply.status(503).send({ + status: "unavailable", + protocol: { supported_versions: [1] }, + database: "unavailable", + }); + } + }); + + app.get("/v1/capabilities", async (request, reply) => { + emptyQuerySchema.parse(request.query); + const teamId = scopedTeamId(request, config.token); + reply.header("Cache-Control", "no-store"); + return getCapabilities(pool, teamId); + }); + + app.get("/v1/members", async (request) => { + emptyQuerySchema.parse(request.query); + return getMembers(pool, scopedTeamId(request, config.token)); + }); + + app.get("/v1/messages", async (request) => { + const teamId = scopedTeamId(request, config.token); + const query = messagesQuerySchema.parse(request.query); + return getMessages(pool, teamId, parseSequence(query.after), query.limit); + }); + + app.post("/v1/messages", async (request) => { + const teamId = scopedTeamId(request, config.token); + const body = postMessagesSchema.parse(request.body); + return postMessages(pool, teamId, body.messages); + }); + + return app; +} + +function requestLog(reply: { log: { error: (value: unknown) => void } }, error: unknown) { + reply.log.error(error); +} + +function statusCode(error: unknown): number | undefined { + if (typeof error !== "object" || error === null || !("statusCode" in error)) { + return undefined; + } + return typeof error.statusCode === "number" ? error.statusCode : undefined; +} diff --git a/server/src/config.ts b/server/src/config.ts new file mode 100644 index 00000000..2760abf5 --- /dev/null +++ b/server/src/config.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +const environmentSchema = z.object({ + DATABASE_URL: z.string().min(1), + AGMSG_SERVER_TOKEN: z.string().min(16), + HOST: z.string().default("127.0.0.1"), + PORT: z.coerce.number().int().min(1).max(65535).default(8787), + LOG_LEVEL: z + .enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"]) + .default("info"), +}); + +export type Config = { + databaseUrl: string; + token: string; + host: string; + port: number; + logLevel: z.infer["LOG_LEVEL"]; +}; + +export function loadConfig(environment: NodeJS.ProcessEnv = process.env): Config { + const parsed = environmentSchema.parse(environment); + return { + databaseUrl: parsed.DATABASE_URL, + token: parsed.AGMSG_SERVER_TOKEN, + host: parsed.HOST, + port: parsed.PORT, + logLevel: parsed.LOG_LEVEL, + }; +} diff --git a/server/src/db.ts b/server/src/db.ts new file mode 100644 index 00000000..59216515 --- /dev/null +++ b/server/src/db.ts @@ -0,0 +1,55 @@ +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { Pool, type PoolClient } from "pg"; +import { v7 as uuidv7 } from "uuid"; + +export function createPool(connectionString: string): Pool { + return new Pool({ connectionString, max: 10 }); +} + +export async function migrate(pool: Pool): Promise { + const migrationPath = fileURLToPath( + new URL("../migrations/001_initial.sql", import.meta.url), + ); + const sql = await readFile(migrationPath, "utf8"); + const client = await pool.connect(); + try { + await client.query("BEGIN"); + await client.query(sql); + await client.query( + `INSERT INTO server_metadata (singleton, server_instance_id) + VALUES (TRUE, $1) + ON CONFLICT (singleton) DO NOTHING`, + [uuidv7()], + ); + await client.query("COMMIT"); + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } +} + +export async function inTransaction( + pool: Pool, + operation: (client: PoolClient) => Promise, + options: { readOnly?: boolean; repeatableRead?: boolean } = {}, +): Promise { + const client = await pool.connect(); + try { + const clauses = [ + options.repeatableRead ? "ISOLATION LEVEL REPEATABLE READ" : "", + options.readOnly ? "READ ONLY" : "", + ].filter(Boolean); + await client.query(`BEGIN${clauses.length > 0 ? ` ${clauses.join(" ")}` : ""}`); + const value = await operation(client); + await client.query("COMMIT"); + return value; + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } +} diff --git a/server/src/errors.ts b/server/src/errors.ts new file mode 100644 index 00000000..f3f9f7e3 --- /dev/null +++ b/server/src/errors.ts @@ -0,0 +1,27 @@ +export class ProtocolError extends Error { + constructor( + readonly statusCode: number, + readonly code: string, + message: string, + readonly details: Record = {}, + readonly binding: { serverInstanceId?: string; teamId?: string } = {}, + ) { + super(message); + this.name = "ProtocolError"; + } +} + +export function errorBody(error: ProtocolError): Record { + return { + protocol_version: 1, + ...(error.binding.serverInstanceId + ? { server_instance_id: error.binding.serverInstanceId } + : {}), + ...(error.binding.teamId ? { team_id: error.binding.teamId } : {}), + error: { + code: error.code, + message: error.message, + details: error.details, + }, + }; +} diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 00000000..f28e27f0 --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,17 @@ +import { createApp } from "./app.js"; +import { loadConfig } from "./config.js"; +import { createPool, migrate } from "./db.js"; + +const config = loadConfig(); +const pool = createPool(config.databaseUrl); +await migrate(pool); + +const app = createApp(pool, config); +const shutdown = async () => { + await app.close(); + await pool.end(); +}; +process.once("SIGINT", () => void shutdown()); +process.once("SIGTERM", () => void shutdown()); + +await app.listen({ host: config.host, port: config.port }); diff --git a/server/src/migrate.ts b/server/src/migrate.ts new file mode 100644 index 00000000..7c8fab6c --- /dev/null +++ b/server/src/migrate.ts @@ -0,0 +1,10 @@ +import { loadConfig } from "./config.js"; +import { createPool, migrate } from "./db.js"; + +const config = loadConfig(); +const pool = createPool(config.databaseUrl); +try { + await migrate(pool); +} finally { + await pool.end(); +} diff --git a/server/src/protocol.ts b/server/src/protocol.ts new file mode 100644 index 00000000..7bda1235 --- /dev/null +++ b/server/src/protocol.ts @@ -0,0 +1,137 @@ +import { createHash } from "node:crypto"; +import { z } from "zod"; + +export const MAX_SEQUENCE = 9_223_372_036_854_775_807n; +export const MAX_REQUEST_BYTES = 2 * 1024 * 1024; +export const MAX_BLOB_BYTES = 1024 * 1024; + +const uuidV4Pattern = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const uuidV7Pattern = + /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const sequencePattern = /^(0|[1-9][0-9]*)$/; +const cipherPattern = /^[a-z0-9][a-z0-9._-]{0,63}$/; +const timestampPattern = + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$/; + +export const uuidV4Schema = z.string().regex(uuidV4Pattern); +export const uuidV7Schema = z.string().regex(uuidV7Pattern); +export const timestampSchema = z.string().regex(timestampPattern); + +export const sequenceSchema = z.string().regex(sequencePattern).refine((value) => { + try { + return BigInt(value) <= MAX_SEQUENCE; + } catch { + return false; + } +}); + +export function parseSequence(value: string): bigint { + return BigInt(sequenceSchema.parse(value)); +} + +function canonicalBlob(value: string): boolean { + if (value.length < 1 || value.length > 1_398_104) return false; + if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) { + return false; + } + const bytes = Buffer.from(value, "base64"); + return bytes.length <= MAX_BLOB_BYTES && bytes.toString("base64") === value; +} + +const keyIdSchema = z.union([ + z.null(), + z.string().refine((value) => { + const bytes = Buffer.byteLength(value, "utf8"); + return ( + value === value.normalize("NFC") && + bytes >= 1 && + bytes <= 256 && + !/[\u0000-\u001f\u007f]/u.test(value) + ); + }), +]); + +export const envelopeSchema = z + .object({ + v: z.number().int().min(0).max(0xffff_ffff), + cipher: z.string().regex(cipherPattern), + key_id: keyIdSchema, + blob: z.string().refine(canonicalBlob), + }) + .strict() + .superRefine((value, context) => { + if (value.cipher === "none" && value.key_id !== null) { + context.addIssue({ + code: "custom", + path: ["key_id"], + message: "key_id must be null for cipher none", + }); + } + }); + +export const messageInputSchema = z + .object({ id: uuidV4Schema, envelope: envelopeSchema }) + .strict(); + +export const postMessagesSchema = z + .object({ messages: z.array(messageInputSchema).min(1).max(1000) }) + .strict(); + +export type Envelope = z.infer; +export type MessageInput = z.infer; + +export const messagesQuerySchema = z.object({ + after: sequenceSchema, + limit: z.preprocess( + (value) => value ?? "100", + z + .string() + .regex(/^[1-9][0-9]*$/) + .transform(Number) + .refine((value) => value >= 1 && value <= 1000), + ), +}).strict(); + +export const agentNameSchema = z.string().refine((value) => { + const scalars = [...value]; + return ( + value === value.normalize("NFC") && + scalars.length >= 1 && + scalars.length <= 128 && + !value.startsWith("-") && + value !== "." && + value !== ".." && + !/[./\\"\[\]\u0000-\u001f\u007f]/u.test(value) + ); +}); + +function u32(value: number): Buffer { + const buffer = Buffer.allocUnsafe(4); + buffer.writeUInt32BE(value); + return buffer; +} + +function sized(value: Buffer): Buffer { + return Buffer.concat([u32(value.length), value]); +} + +export function envelopeDigest(envelope: Envelope): Buffer { + const cipher = Buffer.from(envelope.cipher, "utf8"); + const blob = Buffer.from(envelope.blob, "base64"); + const key = + envelope.key_id === null + ? Buffer.from([0]) + : Buffer.concat([Buffer.from([1]), sized(Buffer.from(envelope.key_id, "utf8"))]); + return createHash("sha256") + .update( + Buffer.concat([ + Buffer.from("agmsg-envelope-v1\0", "ascii"), + u32(envelope.v), + sized(cipher), + key, + sized(blob), + ]), + ) + .digest(); +} diff --git a/server/src/provision.ts b/server/src/provision.ts new file mode 100644 index 00000000..e7941b04 --- /dev/null +++ b/server/src/provision.ts @@ -0,0 +1,189 @@ +import { readFile } from "node:fs/promises"; +import * as duplicateKeyJson from "json-dup-key-validator"; +import { z } from "zod"; +import { loadConfig } from "./config.js"; +import { createPool, inTransaction, migrate } from "./db.js"; +import { agentNameSchema, MAX_SEQUENCE, uuidV7Schema } from "./protocol.js"; + +const registrationSchema = z + .object({ + registration_id: uuidV7Schema, + installation_id: uuidV7Schema, + type: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/), + }) + .strict(); + +const memberSchema = z + .object({ + member_id: uuidV7Schema, + name: agentNameSchema, + registrations: z.array(registrationSchema), + }) + .strict(); + +const manifestSchema = z + .object({ + team_id: uuidV7Schema, + team_name: z + .string() + .refine( + (value) => + value === value.normalize("NFC") && + [...value].length >= 1 && + [...value].length <= 128, + ), + members: z.array(memberSchema), + }) + .strict(); + +const manifestPath = process.argv[2]; +if (!manifestPath) { + throw new Error("Usage: npm run provision -- "); +} + +const manifestSource = await readFile(manifestPath, "utf8"); +const manifest = manifestSchema.parse(duplicateKeyJson.parse(manifestSource, false)); +const memberIds = new Set(); +const memberNames = new Set(); +const registrationIds = new Set(); +for (const member of manifest.members) { + if (memberIds.has(member.member_id) || memberNames.has(member.name)) { + throw new Error("manifest contains duplicate member ID or name"); + } + memberIds.add(member.member_id); + memberNames.add(member.name); + for (const registration of member.registrations) { + if (registrationIds.has(registration.registration_id)) { + throw new Error("manifest contains a duplicate registration ID"); + } + registrationIds.add(registration.registration_id); + } +} + +const config = loadConfig(); +const pool = createPool(config.databaseUrl); +try { + await migrate(pool); + const revision = await inTransaction(pool, async (client) => { + const existing = await client.query<{ members_revision: string }>( + `SELECT members_revision::text FROM teams + WHERE team_id = $1 FOR UPDATE`, + [manifest.team_id], + ); + const currentMembers = new Set( + ( + await client.query<{ member_id: string }>( + "SELECT member_id::text FROM members WHERE team_id = $1", + [manifest.team_id], + ) + ).rows.map((row) => row.member_id), + ); + const currentRegistrations = new Set( + ( + await client.query<{ registration_id: string }>( + "SELECT registration_id::text FROM registrations WHERE team_id = $1", + [manifest.team_id], + ) + ).rows.map((row) => row.registration_id), + ); + + let nextRevision = 0n; + if (existing.rows[0]) { + const current = BigInt(existing.rows[0].members_revision); + if (current === MAX_SEQUENCE) throw new Error("members revision is exhausted"); + nextRevision = current + 1n; + await client.query( + "UPDATE teams SET team_name = $2, members_revision = $3 WHERE team_id = $1", + [manifest.team_id, manifest.team_name, nextRevision.toString()], + ); + } else { + await client.query( + `INSERT INTO teams (team_id, team_name, members_revision) + VALUES ($1, $2, 0)`, + [manifest.team_id, manifest.team_name], + ); + await client.query( + `INSERT INTO team_policy_history + (team_id, policy_revision, effective_from_seq, + accepted_envelope_versions, write_allowed_ciphers) + VALUES ($1, 0, 1, ARRAY[1], ARRAY['none']::TEXT[])`, + [manifest.team_id], + ); + } + + for (const member of manifest.members) { + const memberSeen = await client.query<{ present: boolean }>( + `SELECT TRUE AS present FROM member_identity_history + WHERE team_id = $1 AND member_id = $2 LIMIT 1`, + [manifest.team_id, member.member_id], + ); + if (memberSeen.rows[0] && !currentMembers.has(member.member_id)) { + throw new Error(`member ${member.member_id} is retired`); + } + const nameOwner = await client.query<{ member_id: string }>( + `SELECT member_id::text FROM member_identity_history + WHERE team_id = $1 AND name = $2`, + [manifest.team_id, member.name], + ); + if (nameOwner.rows[0] && nameOwner.rows[0].member_id !== member.member_id) { + throw new Error(`member name ${member.name} is retired by another member`); + } + await client.query( + `INSERT INTO member_identity_history (team_id, member_id, name) + VALUES ($1, $2, $3) ON CONFLICT (team_id, name) DO NOTHING`, + [manifest.team_id, member.member_id, member.name], + ); + for (const registration of member.registrations) { + const owner = await client.query<{ member_id: string }>( + `SELECT member_id::text FROM registration_identity_history + WHERE team_id = $1 AND registration_id = $2`, + [manifest.team_id, registration.registration_id], + ); + if (owner.rows[0] && !currentRegistrations.has(registration.registration_id)) { + throw new Error(`registration ${registration.registration_id} is retired`); + } + if (owner.rows[0] && owner.rows[0].member_id !== member.member_id) { + throw new Error( + `registration ${registration.registration_id} is retired by another member`, + ); + } + await client.query( + `INSERT INTO registration_identity_history + (team_id, registration_id, member_id) + VALUES ($1, $2, $3) + ON CONFLICT (team_id, registration_id) DO NOTHING`, + [manifest.team_id, registration.registration_id, member.member_id], + ); + } + } + + await client.query("DELETE FROM registrations WHERE team_id = $1", [manifest.team_id]); + await client.query("DELETE FROM members WHERE team_id = $1", [manifest.team_id]); + for (const member of manifest.members) { + await client.query( + "INSERT INTO members (team_id, member_id, name) VALUES ($1, $2, $3)", + [manifest.team_id, member.member_id, member.name], + ); + for (const registration of member.registrations) { + await client.query( + `INSERT INTO registrations + (team_id, registration_id, member_id, installation_id, type) + VALUES ($1, $2, $3, $4, $5)`, + [ + manifest.team_id, + registration.registration_id, + member.member_id, + registration.installation_id, + registration.type, + ], + ); + } + } + return nextRevision.toString(); + }); + process.stdout.write( + `${JSON.stringify({ team_id: manifest.team_id, members_revision: revision })}\n`, + ); +} finally { + await pool.end(); +} diff --git a/server/src/retain.ts b/server/src/retain.ts new file mode 100644 index 00000000..de6f2052 --- /dev/null +++ b/server/src/retain.ts @@ -0,0 +1,15 @@ +import { loadConfig } from "./config.js"; +import { createPool, migrate } from "./db.js"; +import { parseSequence, uuidV7Schema } from "./protocol.js"; +import { retainThrough } from "./storage.js"; + +const teamId = uuidV7Schema.parse(process.argv[2]); +const through = parseSequence(process.argv[3] ?? ""); +const config = loadConfig(); +const pool = createPool(config.databaseUrl); +try { + await migrate(pool); + process.stdout.write(`${JSON.stringify(await retainThrough(pool, teamId, through))}\n`); +} finally { + await pool.end(); +} diff --git a/server/src/storage.ts b/server/src/storage.ts new file mode 100644 index 00000000..545527dc --- /dev/null +++ b/server/src/storage.ts @@ -0,0 +1,499 @@ +import type { Pool, PoolClient } from "pg"; +import { ProtocolError } from "./errors.js"; +import { + MAX_SEQUENCE, + envelopeDigest, + type Envelope, + type MessageInput, +} from "./protocol.js"; +import { inTransaction } from "./db.js"; + +type TeamRow = { + team_id: string; + team_name: string; + current_seq: string; + min_available_seq: string; + policy_revision: string; + accepted_envelope_versions: number[]; + write_allowed_ciphers: string[]; + max_blob_bytes: number; + members_revision: string; +}; + +type LiveMessageRow = { + id: string; + team_seq: string; + server_received_at: string; + envelope_v: number; + cipher: string; + key_id: string | null; + blob: string; + envelope_digest: Buffer; +}; + +type ExistingRecord = + | { kind: "live"; row: LiveMessageRow } + | { kind: "tombstone"; sequence: string; digest: Buffer }; + +const timestampSql = `to_char(server_received_at AT TIME ZONE 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`; + +async function serverInstanceId(client: PoolClient): Promise { + const result = await client.query<{ server_instance_id: string }>( + "SELECT server_instance_id::text FROM server_metadata WHERE singleton = TRUE", + ); + const id = result.rows[0]?.server_instance_id; + if (!id) throw new Error("server metadata is not initialized"); + return id; +} + +async function teamRow( + client: PoolClient, + id: string, + lock = false, +): Promise { + const result = await client.query( + `SELECT team_id::text, team_name, current_seq::text, min_available_seq::text, + policy_revision::text, accepted_envelope_versions, + write_allowed_ciphers, max_blob_bytes, members_revision::text + FROM teams WHERE team_id = $1${lock ? " FOR UPDATE" : ""}`, + [id], + ); + return result.rows[0]; +} + +function common(serverId: string, team: TeamRow): Record { + return { + protocol_version: 1, + server_instance_id: serverId, + team_id: team.team_id, + team_name: team.team_name, + min_available_seq: team.min_available_seq, + }; +} + +function notFound(serverId: string, teamId: string): ProtocolError { + return new ProtocolError(404, "team-not-found", "team is not provisioned", {}, { + serverInstanceId: serverId, + teamId, + }); +} + +function envelopeMatches(row: LiveMessageRow, envelope: Envelope): boolean { + return ( + row.envelope_v === envelope.v && + row.cipher === envelope.cipher && + row.key_id === envelope.key_id && + row.blob === envelope.blob + ); +} + +function inputFingerprint(message: MessageInput): string { + return JSON.stringify([ + message.envelope.v, + message.envelope.cipher, + message.envelope.key_id, + message.envelope.blob, + ]); +} + +export async function postMessages( + pool: Pool, + teamId: string, + messages: MessageInput[], +): Promise> { + return inTransaction(pool, async (client) => { + const serverId = await serverInstanceId(client); + const team = await teamRow(client, teamId, true); + if (!team) throw notFound(serverId, teamId); + const binding = { serverInstanceId: serverId, teamId }; + + const firstById = new Map(); + for (const message of messages) { + const first = firstById.get(message.id); + if (first && inputFingerprint(first) !== inputFingerprint(message)) { + throw new ProtocolError( + 409, + "message-uuid-conflict", + "message id is repeated with a different payload", + { id: message.id }, + binding, + ); + } + firstById.set(message.id, first ?? message); + } + + const ids = [...firstById.keys()]; + const liveResult = await client.query( + `SELECT id::text, team_seq::text, ${timestampSql} AS server_received_at, + envelope_v, cipher, key_id, blob, envelope_digest + FROM messages WHERE team_id = $1 AND id = ANY($2::uuid[])`, + [teamId, ids], + ); + const tombstoneResult = await client.query<{ + id: string; + original_team_seq: string; + envelope_digest: Buffer; + }>( + `SELECT id::text, original_team_seq::text, envelope_digest + FROM message_tombstones WHERE team_id = $1 AND id = ANY($2::uuid[])`, + [teamId, ids], + ); + const existing = new Map(); + for (const row of liveResult.rows) existing.set(row.id, { kind: "live", row }); + for (const row of tombstoneResult.rows) { + existing.set(row.id, { + kind: "tombstone", + sequence: row.original_team_seq, + digest: row.envelope_digest, + }); + } + + for (const [id, message] of firstById) { + const record = existing.get(id); + if (!record) continue; + const matches = + record.kind === "live" + ? envelopeMatches(record.row, message.envelope) + : record.digest.equals(envelopeDigest(message.envelope)); + if (!matches) { + throw new ProtocolError( + 409, + "message-uuid-conflict", + "message id already exists with a different payload", + { id }, + binding, + ); + } + } + + const fresh = [...firstById.values()].filter((message) => !existing.has(message.id)); + for (const message of fresh) { + const { envelope, id } = message; + if (envelope.v !== 1 || envelope.cipher !== "none") { + throw new ProtocolError( + 422, + "unsupported-cipher", + "envelope version or cipher is not supported", + { + id, + v: envelope.v, + cipher: envelope.cipher, + accepted_envelope_versions: team.accepted_envelope_versions, + write_allowed_ciphers: team.write_allowed_ciphers, + policy_revision: team.policy_revision, + }, + binding, + ); + } + if ( + !team.accepted_envelope_versions.includes(envelope.v) || + !team.write_allowed_ciphers.includes(envelope.cipher) + ) { + throw new ProtocolError( + 403, + "cipher-policy-violation", + "cipher is not currently write-allowed", + { + id, + v: envelope.v, + cipher: envelope.cipher, + write_allowed_ciphers: team.write_allowed_ciphers, + policy_revision: team.policy_revision, + }, + binding, + ); + } + if (Buffer.from(envelope.blob, "base64").length > team.max_blob_bytes) { + throw new ProtocolError( + 413, + "request-too-large", + "message blob exceeds the team capability limit", + { id, max_blob_bytes: String(team.max_blob_bytes) }, + binding, + ); + } + } + + let next = BigInt(team.current_seq); + if (BigInt(fresh.length) > MAX_SEQUENCE - next) { + throw new ProtocolError( + 507, + "sequence-exhausted", + "team sequence is exhausted", + {}, + binding, + ); + } + + const inserted = new Map(); + for (const message of fresh) { + next += 1n; + const digest = envelopeDigest(message.envelope); + const result = await client.query( + `INSERT INTO messages + (team_id, id, team_seq, envelope_v, cipher, key_id, blob, envelope_digest) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING id::text, team_seq::text, ${timestampSql} AS server_received_at, + envelope_v, cipher, key_id, blob, envelope_digest`, + [ + teamId, + message.id, + next.toString(), + message.envelope.v, + message.envelope.cipher, + message.envelope.key_id, + message.envelope.blob, + digest, + ], + ); + const row = result.rows[0]; + if (!row) throw new Error("insert did not return a message"); + inserted.set(message.id, row); + existing.set(message.id, { kind: "live", row }); + } + if (fresh.length > 0) { + await client.query("UPDATE teams SET current_seq = $2 WHERE team_id = $1", [ + teamId, + next.toString(), + ]); + } + + const seen = new Set(); + const acks = messages.map((message) => { + const record = existing.get(message.id); + if (!record) throw new Error("missing canonical ack record"); + const stored = inserted.has(message.id) && !seen.has(message.id); + seen.add(message.id); + return { + id: message.id, + server_seq: record.kind === "live" ? record.row.team_seq : record.sequence, + disposition: stored ? "stored" : "duplicate", + }; + }); + + return { + ...common(serverId, { ...team, current_seq: next.toString() }), + policy_revision: team.policy_revision, + acks, + }; + }); +} + +export async function retainThrough( + pool: Pool, + teamId: string, + through: bigint, +): Promise> { + return inTransaction(pool, async (client) => { + const serverId = await serverInstanceId(client); + const team = await teamRow(client, teamId, true); + if (!team) throw notFound(serverId, teamId); + const currentFloor = BigInt(team.min_available_seq); + const currentSequence = BigInt(team.current_seq); + if (through < currentFloor || through > currentSequence) { + throw new ProtocolError( + 400, + "invalid-request", + "retention floor must be between the current floor and current sequence", + { + through: through.toString(), + min_available_seq: team.min_available_seq, + current_seq: team.current_seq, + }, + { serverInstanceId: serverId, teamId }, + ); + } + + const tombstones = await client.query( + `INSERT INTO message_tombstones + (team_id, id, original_team_seq, envelope_digest) + SELECT team_id, id, team_seq, envelope_digest + FROM messages + WHERE team_id = $1 AND team_seq <= $2 + RETURNING id`, + [teamId, through.toString()], + ); + const deleted = await client.query( + "DELETE FROM messages WHERE team_id = $1 AND team_seq <= $2", + [teamId, through.toString()], + ); + if (deleted.rowCount !== tombstones.rowCount) { + throw new Error("retention tombstone and deletion counts differ"); + } + await client.query( + "UPDATE teams SET min_available_seq = $2 WHERE team_id = $1", + [teamId, through.toString()], + ); + return { + ...common(serverId, { ...team, min_available_seq: through.toString() }), + retained_through: through.toString(), + tombstones_created: String(tombstones.rowCount ?? 0), + }; + }); +} + +export async function getMessages( + pool: Pool, + teamId: string, + after: bigint, + limit: number, +): Promise> { + return inTransaction( + pool, + async (client) => { + const serverId = await serverInstanceId(client); + const team = await teamRow(client, teamId); + if (!team) throw notFound(serverId, teamId); + if (after < BigInt(team.min_available_seq)) { + throw new ProtocolError( + 410, + "resync-required", + "cursor predates retained history", + { after: after.toString(), min_available_seq: team.min_available_seq }, + { serverInstanceId: serverId, teamId }, + ); + } + const result = await client.query( + `SELECT id::text, team_seq::text, ${timestampSql} AS server_received_at, + envelope_v, cipher, key_id, blob, envelope_digest + FROM messages + WHERE team_id = $1 AND team_seq > $2 + ORDER BY team_seq ASC + LIMIT $3`, + [teamId, after.toString(), limit + 1], + ); + const hasMore = result.rows.length > limit; + const page = result.rows.slice(0, limit); + return { + ...common(serverId, team), + messages: page.map((row) => ({ + server_seq: row.team_seq, + id: row.id, + server_received_at: row.server_received_at, + envelope: { + v: row.envelope_v, + cipher: row.cipher, + key_id: row.key_id, + blob: row.blob, + }, + })), + next_after: page.at(-1)?.team_seq ?? after.toString(), + has_more: hasMore, + }; + }, + { readOnly: true, repeatableRead: true }, + ); +} + +export async function getCapabilities( + pool: Pool, + teamId: string, +): Promise> { + return inTransaction( + pool, + async (client) => { + const serverId = await serverInstanceId(client); + const team = await teamRow(client, teamId); + if (!team) throw notFound(serverId, teamId); + const historyResult = await client.query<{ + policy_revision: string; + effective_from_seq: string; + accepted_envelope_versions: number[]; + write_allowed_ciphers: string[]; + }>( + `SELECT policy_revision::text, effective_from_seq::text, + accepted_envelope_versions, write_allowed_ciphers + FROM ( + SELECT DISTINCT ON (effective_from_seq) + policy_revision, effective_from_seq, + accepted_envelope_versions, write_allowed_ciphers + FROM team_policy_history + WHERE team_id = $1 + ORDER BY effective_from_seq, policy_revision DESC + ) effective + ORDER BY effective_from_seq, policy_revision`, + [teamId], + ); + if (historyResult.rows.length > 4096) { + throw new Error("team policy history exceeds protocol limit"); + } + const current = BigInt(team.current_seq); + return { + ...common(serverId, team), + current_seq: team.current_seq, + next_sequence_boundary: + current === MAX_SEQUENCE ? null : (current + 1n).toString(), + accepted_envelope_versions: team.accepted_envelope_versions, + write_allowed_ciphers: team.write_allowed_ciphers, + policy_revision: team.policy_revision, + effective_from_seq: + historyResult.rows.at(-1)?.effective_from_seq ?? "1", + max_blob_bytes: String(team.max_blob_bytes), + policy_history: historyResult.rows, + }; + }, + { readOnly: true, repeatableRead: true }, + ); +} + +export async function getMembers( + pool: Pool, + teamId: string, +): Promise> { + return inTransaction( + pool, + async (client) => { + const serverId = await serverInstanceId(client); + const team = await teamRow(client, teamId); + if (!team) throw notFound(serverId, teamId); + const members = await client.query<{ member_id: string; name: string }>( + `SELECT member_id::text, name FROM members + WHERE team_id = $1 ORDER BY member_id`, + [teamId], + ); + const registrations = await client.query<{ + registration_id: string; + member_id: string; + installation_id: string; + type: string; + }>( + `SELECT registration_id::text, member_id::text, installation_id::text, type + FROM registrations WHERE team_id = $1 + ORDER BY registration_id`, + [teamId], + ); + return { + ...common(serverId, team), + members_revision: team.members_revision, + members: members.rows.map((member) => ({ + member_id: member.member_id, + name: member.name, + registrations: registrations.rows + .filter((registration) => registration.member_id === member.member_id) + .map(({ member_id: _memberId, ...registration }) => registration), + })), + }; + }, + { readOnly: true, repeatableRead: true }, + ); +} + +export async function health(pool: Pool): Promise<{ + status: "ok"; + server_instance_id: string; + protocol: { supported_versions: number[] }; + database: "ok"; +}> { + const client = await pool.connect(); + try { + return { + status: "ok", + server_instance_id: await serverInstanceId(client), + protocol: { supported_versions: [1] }, + database: "ok", + }; + } finally { + client.release(); + } +} diff --git a/server/src/types/json-dup-key-validator.d.ts b/server/src/types/json-dup-key-validator.d.ts new file mode 100644 index 00000000..b4f068f2 --- /dev/null +++ b/server/src/types/json-dup-key-validator.d.ts @@ -0,0 +1,7 @@ +declare module "json-dup-key-validator" { + export function parse(source: string, allowDuplicatedKeys?: boolean): unknown; + export function validate( + source: string, + allowDuplicatedKeys?: boolean, + ): Error | undefined; +} diff --git a/server/test/server.integration.test.ts b/server/test/server.integration.test.ts new file mode 100644 index 00000000..1ba27e22 --- /dev/null +++ b/server/test/server.integration.test.ts @@ -0,0 +1,481 @@ +import { randomBytes } from "node:crypto"; +import { execFile } from "node:child_process"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { Pool } from "pg"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { createApp } from "../src/app.js"; +import type { Config } from "../src/config.js"; +import { migrate } from "../src/db.js"; +import { retainThrough } from "../src/storage.js"; + +const databaseUrl = process.env.TEST_DATABASE_URL; +const describeDatabase = databaseUrl ? describe : describe.skip; +const execFileAsync = promisify(execFile); + +describeDatabase("remote storage HTTP API v1", () => { + const schema = `agmsg_test_${randomBytes(8).toString("hex")}`; + const token = "integration-test-token-32-bytes"; + const teamId = "018f3f7e-0000-7000-8000-000000000001"; + const memberId = "018f3f7e-0000-7000-8000-000000000010"; + const registrationId = "018f3f7e-0000-7000-8000-000000000011"; + const installationId = "018f3f7e-0000-7000-8000-000000000012"; + let admin: Pool; + let pool: Pool; + let app: ReturnType; + + const config: Config = { + databaseUrl: databaseUrl ?? "", + token, + host: "127.0.0.1", + port: 8787, + logLevel: "silent", + }; + + const headers = { + authorization: `Bearer ${token}`, + "agmsg-protocol-version": "1", + "agmsg-team-id": teamId, + }; + + beforeAll(async () => { + admin = new Pool({ connectionString: databaseUrl }); + await admin.query(`CREATE SCHEMA ${schema}`); + pool = new Pool({ + connectionString: databaseUrl, + options: `-c search_path=${schema}`, + }); + await migrate(pool); + await pool.query( + `INSERT INTO teams (team_id, team_name) VALUES ($1, 'example-team')`, + [teamId], + ); + await pool.query( + `INSERT INTO team_policy_history + (team_id, policy_revision, effective_from_seq, + accepted_envelope_versions, write_allowed_ciphers) + VALUES ($1, 0, 1, ARRAY[1], ARRAY['none']::TEXT[])`, + [teamId], + ); + await pool.query( + `INSERT INTO members (team_id, member_id, name) + VALUES ($1, $2, 'worker-1')`, + [teamId, memberId], + ); + await pool.query( + `INSERT INTO registrations + (team_id, registration_id, member_id, installation_id, type) + VALUES ($1, $2, $3, $4, 'codex')`, + [teamId, registrationId, memberId, installationId], + ); + app = createApp(pool, config); + await app.ready(); + }); + + afterAll(async () => { + await app.close(); + await pool.end(); + if (!/^agmsg_test_[0-9a-f]{16}$/.test(schema)) { + throw new Error("refusing to remove an unexpected test schema"); + } + await admin.query(`DROP SCHEMA ${schema} CASCADE`); + await admin.end(); + }); + + function message(id: string, text: string) { + const plaintext = JSON.stringify({ + body: text, + created_at: "2026-07-20T06:30:00.000000Z", + from_agent: "leader", + to_agent: "worker-1", + }); + return { + id, + envelope: { + v: 1, + cipher: "none", + key_id: null, + blob: Buffer.from(plaintext).toString("base64"), + }, + }; + } + + it("reports readiness and fixes the response protocol version", async () => { + const response = await app.inject({ method: "GET", url: "/v1/health" }); + expect(response.statusCode).toBe(200); + expect(response.headers["agmsg-protocol-version"]).toBe("1"); + expect(response.json()).toMatchObject({ + status: "ok", + protocol: { supported_versions: [1] }, + database: "ok", + }); + }); + + it("requires matching protocol, credentials, and immutable team ID", async () => { + const noVersion = await app.inject({ + method: "GET", + url: "/v1/members", + headers: { authorization: `Bearer ${token}`, "agmsg-team-id": teamId }, + }); + expect(noVersion.statusCode).toBe(426); + expect(noVersion.json().error.code).toBe("unsupported-protocol-version"); + + const noAuth = await app.inject({ + method: "GET", + url: "/v1/members", + headers: { + "agmsg-protocol-version": "1", + "agmsg-team-id": teamId, + }, + }); + expect(noAuth.statusCode).toBe(401); + }); + + it("stores a batch atomically and returns complete input-order acknowledgements", async () => { + const first = message("550e8400-e29b-41d4-a716-446655440000", "first"); + const response = await app.inject({ + method: "POST", + url: "/v1/messages", + headers, + payload: { messages: [first, first] }, + }); + expect(response.statusCode).toBe(200); + expect(response.json().acks).toEqual([ + { id: first.id, server_seq: "1", disposition: "stored" }, + { id: first.id, server_seq: "1", disposition: "duplicate" }, + ]); + + const replay = await app.inject({ + method: "POST", + url: "/v1/messages", + headers, + payload: { messages: [first] }, + }); + expect(replay.json().acks[0]).toEqual({ + id: first.id, + server_seq: "1", + disposition: "duplicate", + }); + + const conflict = await app.inject({ + method: "POST", + url: "/v1/messages", + headers, + payload: { + messages: [ + message("750e8400-e29b-41d4-a716-446655440001", "would-roll-back"), + message(first.id, "different"), + ], + }, + }); + expect(conflict.statusCode).toBe(409); + expect(conflict.json()).toMatchObject({ + team_id: teamId, + error: { code: "message-uuid-conflict" }, + }); + + const count = await pool.query<{ count: string }>( + "SELECT count(*)::text AS count FROM messages WHERE team_id = $1", + [teamId], + ); + expect(count.rows[0]?.count).toBe("1"); + }); + + it("allocates team sequence without a rollback gap and pages one snapshot", async () => { + const second = message("750e8400-e29b-41d4-a716-446655440002", "second"); + const stored = await app.inject({ + method: "POST", + url: "/v1/messages", + headers, + payload: { messages: [second] }, + }); + expect(stored.json().acks[0].server_seq).toBe("2"); + + const page = await app.inject({ + method: "GET", + url: "/v1/messages?after=0&limit=1", + headers, + }); + expect(page.statusCode).toBe(200); + expect(page.json()).toMatchObject({ + next_after: "1", + has_more: true, + messages: [{ server_seq: "1" }], + }); + expect(page.json().messages[0].server_received_at).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$/, + ); + }); + + it("advertises one-snapshot capabilities and operator-provisioned members", async () => { + const capabilities = await app.inject({ + method: "GET", + url: "/v1/capabilities", + headers, + }); + expect(capabilities.statusCode).toBe(200); + expect(capabilities.headers["cache-control"]).toBe("no-store"); + expect(capabilities.json()).toMatchObject({ + current_seq: "2", + next_sequence_boundary: "3", + accepted_envelope_versions: [1], + write_allowed_ciphers: ["none"], + policy_revision: "0", + effective_from_seq: "1", + policy_history: [{ policy_revision: "0", effective_from_seq: "1" }], + }); + + const members = await app.inject({ + method: "GET", + url: "/v1/members", + headers, + }); + expect(members.json()).toMatchObject({ + members_revision: "0", + members: [ + { + member_id: memberId, + name: "worker-1", + registrations: [{ registration_id: registrationId, type: "codex" }], + }, + ], + }); + }); + + it("serializes concurrent writers on the team row", async () => { + const writes = await Promise.all( + [ + message("750e8400-e29b-41d4-a716-446655440005", "concurrent-a"), + message("750e8400-e29b-41d4-a716-446655440006", "concurrent-b"), + ].map((entry) => + app.inject({ + method: "POST", + url: "/v1/messages", + headers, + payload: { messages: [entry] }, + }), + ), + ); + expect(writes.map((response) => response.statusCode)).toEqual([200, 200]); + expect( + writes + .map((response) => response.json().acks[0].server_seq) + .sort((left, right) => Number(left) - Number(right)), + ).toEqual(["3", "4"]); + }); + + it("retains atomically under the writer lock and keeps tombstones idempotent", async () => { + await pool.query( + `CREATE FUNCTION fail_tombstone_insert() RETURNS trigger AS $$ + BEGIN RAISE EXCEPTION 'injected retention failure'; END; + $$ LANGUAGE plpgsql`, + ); + await pool.query( + `CREATE TRIGGER fail_tombstone_insert + BEFORE INSERT ON message_tombstones + FOR EACH ROW EXECUTE FUNCTION fail_tombstone_insert()`, + ); + await expect(retainThrough(pool, teamId, 1n)).rejects.toThrow( + /injected retention failure/, + ); + const rolledBack = await pool.query<{ + messages: string; + tombstones: string; + floor: string; + }>( + `SELECT + (SELECT count(*)::text FROM messages WHERE team_id = $1) AS messages, + (SELECT count(*)::text FROM message_tombstones WHERE team_id = $1) AS tombstones, + (SELECT min_available_seq::text FROM teams WHERE team_id = $1) AS floor`, + [teamId], + ); + expect(rolledBack.rows[0]).toEqual({ messages: "4", tombstones: "0", floor: "0" }); + await pool.query("DROP TRIGGER fail_tombstone_insert ON message_tombstones"); + await pool.query("DROP FUNCTION fail_tombstone_insert()"); + + const concurrent = message("750e8400-e29b-41d4-a716-446655440007", "after-floor"); + const [retained, posted] = await Promise.all([ + retainThrough(pool, teamId, 4n), + app.inject({ + method: "POST", + url: "/v1/messages", + headers, + payload: { messages: [concurrent] }, + }), + ]); + expect(retained).toMatchObject({ + min_available_seq: "4", + retained_through: "4", + tombstones_created: "4", + }); + expect(posted.statusCode).toBe(200); + expect(posted.json().acks[0].server_seq).toBe("5"); + + const belowFloor = await app.inject({ + method: "GET", + url: "/v1/messages?after=0", + headers, + }); + expect(belowFloor.statusCode).toBe(410); + expect(belowFloor.json().error.code).toBe("resync-required"); + + const replay = await app.inject({ + method: "POST", + url: "/v1/messages", + headers, + payload: { + messages: [message("550e8400-e29b-41d4-a716-446655440000", "first")], + }, + }); + expect(replay.json().acks[0]).toMatchObject({ + server_seq: "1", + disposition: "duplicate", + }); + + await pool.query( + "UPDATE teams SET write_allowed_ciphers = ARRAY[]::TEXT[] WHERE team_id = $1", + [teamId], + ); + const duplicateUnderNewPolicy = await app.inject({ + method: "POST", + url: "/v1/messages", + headers, + payload: { + messages: [message("550e8400-e29b-41d4-a716-446655440000", "first")], + }, + }); + expect(duplicateUnderNewPolicy.statusCode).toBe(200); + expect(duplicateUnderNewPolicy.json().acks[0].disposition).toBe("duplicate"); + const rejectedFresh = await app.inject({ + method: "POST", + url: "/v1/messages", + headers, + payload: { messages: [message("750e8400-e29b-41d4-a716-446655440008", "fresh")] }, + }); + expect(rejectedFresh.statusCode).toBe(403); + await pool.query( + "UPDATE teams SET write_allowed_ciphers = ARRAY['none']::TEXT[] WHERE team_id = $1", + [teamId], + ); + + const invalidVersion = message( + "550e8400-e29b-41d4-a716-446655440000", + "first", + ); + invalidVersion.envelope.v = 0x1_0000_0000; + const outOfRange = await app.inject({ + method: "POST", + url: "/v1/messages", + headers, + payload: { messages: [invalidVersion] }, + }); + expect(outOfRange.statusCode).toBe(400); + expect(outOfRange.json().error.code).toBe("invalid-request"); + }); + + it("atomically provisions the operator roster and permanently retires IDs", async () => { + const directory = await mkdtemp(join(tmpdir(), "agmsg-provision-test-")); + const manifestPath = join(directory, "team.json"); + const provisionTeam = "018f3f7e-0000-7000-8000-000000000101"; + const provisionMember = "018f3f7e-0000-7000-8000-000000000110"; + const provisionRegistration = "018f3f7e-0000-7000-8000-000000000111"; + const connection = new URL(databaseUrl ?? ""); + connection.searchParams.set("options", `-c search_path=${schema}`); + const environment = { + ...process.env, + DATABASE_URL: connection.toString(), + AGMSG_SERVER_TOKEN: token, + }; + const runProvision = () => + execFileAsync( + process.execPath, + ["node_modules/tsx/dist/cli.mjs", "src/provision.ts", manifestPath], + { cwd: process.cwd(), env: environment }, + ); + + try { + const member = { + member_id: provisionMember, + name: "provisioned-worker", + registrations: [ + { + registration_id: provisionRegistration, + installation_id: "018f3f7e-0000-7000-8000-000000000112", + type: "codex", + }, + ], + }; + await writeFile( + manifestPath, + JSON.stringify({ + team_id: provisionTeam, + team_name: "provisioned-team", + members: [member], + }), + ); + const first = await runProvision(); + expect(JSON.parse(first.stdout)).toMatchObject({ members_revision: "0" }); + + await writeFile( + manifestPath, + JSON.stringify({ + team_id: provisionTeam, + team_name: "provisioned-team", + members: [], + }), + ); + const second = await runProvision(); + expect(JSON.parse(second.stdout)).toMatchObject({ members_revision: "1" }); + + await writeFile( + manifestPath, + JSON.stringify({ + team_id: provisionTeam, + team_name: "provisioned-team", + members: [member], + }), + ); + await expect(runProvision()).rejects.toThrow(/retired/); + } finally { + if (!directory.startsWith(join(tmpdir(), "agmsg-provision-test-"))) { + throw new Error("refusing to remove an unexpected temporary directory"); + } + await rm(directory, { recursive: true }); + } + }); + + it("rejects duplicate JSON keys and rolls back a sequence-crossing batch", async () => { + const duplicateKeys = await app.inject({ + method: "POST", + url: "/v1/messages", + headers: { ...headers, "content-type": "application/json" }, + payload: '{"messages":[],"messages":[]}', + }); + expect(duplicateKeys.statusCode).toBe(400); + + await pool.query( + "UPDATE teams SET current_seq = 9223372036854775806 WHERE team_id = $1", + [teamId], + ); + const exhausted = await app.inject({ + method: "POST", + url: "/v1/messages", + headers, + payload: { + messages: [ + message("750e8400-e29b-41d4-a716-446655440003", "a"), + message("750e8400-e29b-41d4-a716-446655440004", "b"), + ], + }, + }); + expect(exhausted.statusCode).toBe(507); + expect(exhausted.json().error.code).toBe("sequence-exhausted"); + const rows = await pool.query<{ count: string }>( + "SELECT count(*)::text AS count FROM messages WHERE id = ANY($1::uuid[])", + [["750e8400-e29b-41d4-a716-446655440003", "750e8400-e29b-41d4-a716-446655440004"]], + ); + expect(rows.rows[0]?.count).toBe("0"); + }); +}); diff --git a/server/tsconfig.build.json b/server/tsconfig.build.json new file mode 100644 index 00000000..840014bb --- /dev/null +++ b/server/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"], + "exclude": ["test"] +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 00000000..0449bc0e --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2023"], + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": ".", + "declaration": true, + "sourceMap": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/server/vitest.config.ts b/server/vitest.config.ts new file mode 100644 index 00000000..9126c1ee --- /dev/null +++ b/server/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/**/*.test.ts"], + exclude: ["dist/**", "node_modules/**"], + }, +}); diff --git a/site/src/pages/principles.astro b/site/src/pages/principles.astro new file mode 100644 index 00000000..3e4cea47 --- /dev/null +++ b/site/src/pages/principles.astro @@ -0,0 +1,87 @@ +--- +import "../styles/global.css"; +import fs from "node:fs"; +import { fileURLToPath } from "node:url"; + +// Renders the repo-root PRINCIPLES.md so the site and the doc never drift. +// PRINCIPLES.md is EN-only for now (see the doc itself), so this page is +// unprefixed at "/principles" only -- no [lang] variants yet. +const principlesPath = fileURLToPath(new URL("../../../PRINCIPLES.md", import.meta.url)); +const raw = fs.readFileSync(principlesPath, "utf8"); + +// Minimal structural parse, not a general markdown renderer -- PRINCIPLES.md +// has a fixed shape (H1 title, one intro paragraph, numbered "## " sections, +// a closing "---" + one italic paragraph) and rendering it structurally +// keeps this page's logic auditable without adding a markdown dependency. +const withoutTitle = raw.replace(/^#\s+agmsg principles\s*\n+/, ""); +const [body, closing] = withoutTitle.split(/\n---\n/); +const [intro, ...sectionChunks] = body.trim().split(/\n##\s+/); +const sections = sectionChunks.map((chunk) => { + const newlineIndex = chunk.indexOf("\n"); + return { + heading: chunk.slice(0, newlineIndex).trim(), + body: chunk.slice(newlineIndex).trim(), + }; +}); +const closingText = (closing || "").trim().replace(/^\*|\*$/g, ""); + +const siteUrl = "https://agmsg.cc"; +const pageTitle = "Principles — agmsg"; +const pageDesc = "The commitments agmsg holds itself to as the project grows: local-first, an open protocol, no lock-in, structural encryption."; +const canonicalUrl = `${siteUrl}/principles`; +--- + + + + + + + {pageTitle} + + + + + + + + + + + + + + + + + + + + + + + +
+ ← agmsg + +

Principles

+

{intro.trim()}

+ +
+ {sections.map((s) => ( +
+

{s.heading}

+

{s.body}

+
+ ))} +
+ + {closingText && ( +

{closingText}

+ )} + +

+ Source: PRINCIPLES.md in the repo. +

+
+ +