From 16a00b922ec5ee7d5d9566fe2533e415ba441e51 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 26 Sep 2026 15:01:56 +0000 Subject: [PATCH 1/7] test(reporting): add auditable interoperability orchestration --- docs/reporting-interop-matrix.md | 505 ++++ .../ci/reporting_interop/applicability.json | 155 + .../controller-operation-map.json | 276 ++ .../ci/reporting_interop/core_scenario.json | 31 + scripts/ci/reporting_interop/corpus.json | 254 ++ .../npm/rc41/package-lock.json | 1729 +++++++++++ .../reporting_interop/npm/rc41/package.json | 8 + .../npm/rc42/package-lock.json | 1730 +++++++++++ .../reporting_interop/npm/rc42/package.json | 15 + .../npm/rc44/package-lock.json | 1771 +++++++++++ .../reporting_interop/npm/rc44/package.json | 15 + .../npm/rc45/package-lock.json | 1770 +++++++++++ .../reporting_interop/npm/rc45/package.json | 8 + .../official_precedence/cases.json | 24 + .../official_precedence/fixture.json | 302 ++ .../official_precedence/pins-rc45.json | 17 + .../official_precedence/pins.json | 17 + .../official_precedence/repro-candidate.cjs | 12 + .../official_precedence/repro-rc42.cjs | 305 ++ scripts/ci/reporting_interop/pins.json | 237 ++ ...requirements-py310-pydantic2130-mcp200.txt | 57 + ...requirements-py310-pydantic2135-mcp220.txt | 60 + .../reporting_interop/python_core_client.py | 121 + .../reporting_interop/python_core_server.py | 302 ++ .../python_dependency_floor_probe.py | 143 + .../run_foundation_matrix.py | 2582 +++++++++++++++++ .../resource_location_python.original.py.txt | 11 + .../security/resource_location_sentinels.json | 19 + .../security/resource_location_typescript.cjs | 13 + .../signing/crossverify_python.py | 40 + .../signing/crossverify_typescript.cjs | 23 + .../signing/run-signing-vectors.cjs | 218 ++ .../signing/sign_webhook_typescript.cjs | 18 + .../signer_roundtrip_python.original.py.txt | 37 + .../signing/verifier-server.cjs | 62 + .../signing/webhook_receiver_client.py | 233 ++ .../signing/webhook_receiver_server.py | 311 ++ .../signing/webhook_revocation_state.py | 76 + .../webhook_vectors_python.original.py.txt | 58 + .../signing/webhook_vectors_typescript.cjs | 58 + .../storyboard_orchestration.py | 1440 +++++++++ .../ci/reporting_interop/ts_adcp_version.cjs | 33 + .../ts_controlled_reporting_fixture.cjs | 560 ++++ .../ts_controlled_reporting_http_probe.cjs | 189 ++ .../ts_controlled_reporting_probe.cjs | 271 ++ .../ts_controlled_reporting_server.cjs | 362 +++ .../ci/reporting_interop/ts_core_buyer.cjs | 294 ++ .../ci/reporting_interop/ts_core_server.cjs | 643 ++++ .../ts_managed_reporting_fixture.cjs | 471 +++ .../ts_managed_reporting_http_probe.cjs | 262 ++ .../ts_managed_reporting_probe.cjs | 342 +++ .../ts_managed_reporting_server.cjs | 385 +++ scripts/ci/reporting_interop/ts_mcp_buyer.cjs | 280 ++ .../ci/reporting_interop/ts_package_probe.cjs | 165 ++ .../ts_primary_facade_gap.cjs | 125 + .../reporting_interop/ts_private_inputs.cjs | 66 + .../ci/reporting_interop/ts_probe_abort.cjs | 42 + .../ts_reporting_controller_probe.cjs | 417 +++ .../ts_storyboard_inventory.cjs | 227 ++ scripts/ci/reporting_interop_inputs.json | 121 + scripts/ci/reporting_interop_matrix.py | 1107 +++++++ tests/test_reporting_interop_corpus.py | 1269 ++++++++ tests/test_reporting_interop_matrix.py | 287 ++ ...test_reporting_storyboard_orchestration.py | 1740 +++++++++++ 64 files changed, 24721 insertions(+) create mode 100644 docs/reporting-interop-matrix.md create mode 100644 scripts/ci/reporting_interop/applicability.json create mode 100644 scripts/ci/reporting_interop/controller-operation-map.json create mode 100644 scripts/ci/reporting_interop/core_scenario.json create mode 100644 scripts/ci/reporting_interop/corpus.json create mode 100644 scripts/ci/reporting_interop/npm/rc41/package-lock.json create mode 100644 scripts/ci/reporting_interop/npm/rc41/package.json create mode 100644 scripts/ci/reporting_interop/npm/rc42/package-lock.json create mode 100644 scripts/ci/reporting_interop/npm/rc42/package.json create mode 100644 scripts/ci/reporting_interop/npm/rc44/package-lock.json create mode 100644 scripts/ci/reporting_interop/npm/rc44/package.json create mode 100644 scripts/ci/reporting_interop/npm/rc45/package-lock.json create mode 100644 scripts/ci/reporting_interop/npm/rc45/package.json create mode 100644 scripts/ci/reporting_interop/official_precedence/cases.json create mode 100644 scripts/ci/reporting_interop/official_precedence/fixture.json create mode 100644 scripts/ci/reporting_interop/official_precedence/pins-rc45.json create mode 100644 scripts/ci/reporting_interop/official_precedence/pins.json create mode 100644 scripts/ci/reporting_interop/official_precedence/repro-candidate.cjs create mode 100644 scripts/ci/reporting_interop/official_precedence/repro-rc42.cjs create mode 100644 scripts/ci/reporting_interop/pins.json create mode 100644 scripts/ci/reporting_interop/python/requirements-py310-pydantic2130-mcp200.txt create mode 100644 scripts/ci/reporting_interop/python/requirements-py310-pydantic2135-mcp220.txt create mode 100644 scripts/ci/reporting_interop/python_core_client.py create mode 100644 scripts/ci/reporting_interop/python_core_server.py create mode 100644 scripts/ci/reporting_interop/python_dependency_floor_probe.py create mode 100644 scripts/ci/reporting_interop/run_foundation_matrix.py create mode 100644 scripts/ci/reporting_interop/security/resource_location_python.original.py.txt create mode 100644 scripts/ci/reporting_interop/security/resource_location_sentinels.json create mode 100644 scripts/ci/reporting_interop/security/resource_location_typescript.cjs create mode 100644 scripts/ci/reporting_interop/signing/crossverify_python.py create mode 100644 scripts/ci/reporting_interop/signing/crossverify_typescript.cjs create mode 100644 scripts/ci/reporting_interop/signing/run-signing-vectors.cjs create mode 100644 scripts/ci/reporting_interop/signing/sign_webhook_typescript.cjs create mode 100644 scripts/ci/reporting_interop/signing/signer_roundtrip_python.original.py.txt create mode 100644 scripts/ci/reporting_interop/signing/verifier-server.cjs create mode 100644 scripts/ci/reporting_interop/signing/webhook_receiver_client.py create mode 100644 scripts/ci/reporting_interop/signing/webhook_receiver_server.py create mode 100644 scripts/ci/reporting_interop/signing/webhook_revocation_state.py create mode 100644 scripts/ci/reporting_interop/signing/webhook_vectors_python.original.py.txt create mode 100644 scripts/ci/reporting_interop/signing/webhook_vectors_typescript.cjs create mode 100644 scripts/ci/reporting_interop/storyboard_orchestration.py create mode 100644 scripts/ci/reporting_interop/ts_adcp_version.cjs create mode 100644 scripts/ci/reporting_interop/ts_controlled_reporting_fixture.cjs create mode 100644 scripts/ci/reporting_interop/ts_controlled_reporting_http_probe.cjs create mode 100644 scripts/ci/reporting_interop/ts_controlled_reporting_probe.cjs create mode 100644 scripts/ci/reporting_interop/ts_controlled_reporting_server.cjs create mode 100644 scripts/ci/reporting_interop/ts_core_buyer.cjs create mode 100755 scripts/ci/reporting_interop/ts_core_server.cjs create mode 100644 scripts/ci/reporting_interop/ts_managed_reporting_fixture.cjs create mode 100644 scripts/ci/reporting_interop/ts_managed_reporting_http_probe.cjs create mode 100644 scripts/ci/reporting_interop/ts_managed_reporting_probe.cjs create mode 100644 scripts/ci/reporting_interop/ts_managed_reporting_server.cjs create mode 100644 scripts/ci/reporting_interop/ts_mcp_buyer.cjs create mode 100644 scripts/ci/reporting_interop/ts_package_probe.cjs create mode 100644 scripts/ci/reporting_interop/ts_primary_facade_gap.cjs create mode 100644 scripts/ci/reporting_interop/ts_private_inputs.cjs create mode 100644 scripts/ci/reporting_interop/ts_probe_abort.cjs create mode 100644 scripts/ci/reporting_interop/ts_reporting_controller_probe.cjs create mode 100644 scripts/ci/reporting_interop/ts_storyboard_inventory.cjs create mode 100644 scripts/ci/reporting_interop_inputs.json create mode 100644 scripts/ci/reporting_interop_matrix.py create mode 100644 tests/test_reporting_interop_corpus.py create mode 100644 tests/test_reporting_interop_matrix.py create mode 100644 tests/test_reporting_storyboard_orchestration.py diff --git a/docs/reporting-interop-matrix.md b/docs/reporting-interop-matrix.md new file mode 100644 index 000000000..0d9fd78a6 --- /dev/null +++ b/docs/reporting-interop-matrix.md @@ -0,0 +1,505 @@ +# Reliable Reporting foundation interoperability + +This is the staged-foundation qualification harness for +[#1199](https://github.com/adcontextprotocol/adcp-client-python/issues/1199). +It is not final compatibility acceptance. Issue #1172 service composition, +`client.reporting`, and #1182 provisional re-read remain explicitly deferred. + +## Immutable inputs + +The reviewed coordinates are in `scripts/ci/reporting_interop/pins.json`. + +This harness source is ported to accepted Python main +`940c95e0c2d93758ed334b3edff59cbe933362d4`. Its retained execution pins do +not move from source ancestry alone. PR #1208 head `a124afd3` still has no +final artifact pin. Published TypeScript rc.47 is selected only as the +developmental rc.6-aligned Q1-Q4 candidate; it has not been installed, locked, +executed, promoted, or accepted by this harness. + +- Python development candidate: exact PR #1208 head `25e0c7278a19493345975881d1578c76fc64dc1b`, + tree `bcc4ad30ea4ee43d7b82f39e12f60b5a3ac2aead`, built as an installed wheel or + sdist-derived wheel before publication. Registry publication is not required + for this prepublication candidate; the bytes supplied to a run are. +- Retained historical executable TypeScript input: exact published + `@adcp/sdk@14.0.0-rc.45`, SHA-256 + `a0952ed8edaaad958bdb8f474c4f5cb68333ee272e7a8f3419d12930e9c57e6b`, + integrity + `sha512-ywglF0pBXDUfxW6IUdXMf+xKAkIKFezxKf+Jq7gDuY2NdqCg6Ctgi496pUYk23oFiBEe8NRjzWadjWzSo/+J7w==`, + publishing commit `94171fe20d632f027eb362604c715ed361fadb51`, and + approved tree `8d3c29d9a1779f04520c33cdc9ed3e15e5296cdd`. Its harness-owned + lock is checked in under `scripts/ci/reporting_interop/npm/rc45/`. Rc.42 + remains the exact historical Matrix12 candidate and is never rewritten as + rc.45 evidence. Rc.45 pins protocol rc.4 and is therefore ineligible for + required rc.6 Q-cell credit even when its archive and installed members are + intact. +- Developmentally selected TypeScript rc.6 candidate: + `@adcp/sdk@14.0.0-rc.47`, SHA-256 + `1b14aeddab973809f1850e189f833ef645d061e25c82d74046c97fd92d346e49`, + integrity + `sha512-Zi2r9CZ2HJmB7i8XTlt5Z1QrVq+nnKkTcl8fSQUkjsi3ChhVtR24BIu/6Gg7yz5ctBWfMR2Lxqn4wv8kh3Wygw==`, + registry git head `b0d2886f0f5b8668fc134568c4cd22a4ef89e2fa`, and tree + `12515822c5814dcf2a0bb3c4d92e7b3ad023df46`. Independent source review + approved the frozen calendar behavior and T1 correction. This coordinate + is not executable harness input until its own lock and complete installed + member binding are selected; rc.45 cannot substitute for that missing input. + No executable row is rebound from rc.45 and no prior result transfers. +- Previous compatible TypeScript skew input: exact `@adcp/sdk@14.0.0-rc.41`, + integrity + `sha512-Qfs+ujKBpIjf7m9jcuzxvN/XCXUssxhR5xCvxCq4oqMR3aMJjnuTgzjxltPtv+A6LOxwmX1ia8sMRdhVSehdBg==`, + from the checked-in `scripts/ci/reporting_interop/npm/rc41/` lock. The + previous Python input is the released b15 wheel installed by local path and + asserted as SHA-256 `608636a4fe774bc4846b5bb0eab6367d0b8e69f6f371aa6085845d9f8329f48f`; + its occupied version string is never used as candidate identity. +- Protocol: signed `3.2.0-rc.4` bundle SHA-256 + `773bee016d279345d6fae91a0ce684a22ca9b81c2edd2cdb9a71dbfea65954d2`. +- Runtime floors: CPython 3.10 and Node 22.12.0. Provenance tooling that needs a + newer Node runtime is separate from the interoperability floor. + +Historical rc.42 and supplemental rc.44 identities remain recorded with their +own locks and results. Neither is a substitute for rc.45, and rc.45 does not +retroactively change their results. PyPI `adcp==7.0.2` is a visible unsupported +latest-stable canary, not a reporting pass. + +The candidate still declares the already published `8.0.0b15` version. A +candidate runtime must therefore expose `direct_url.json` for the exact local +artifact path supplied to the runner; version-only or index-resolved installs +are rejected. Each independently built artifact retains its own SHA-256, +source tree, dependency set, and runtime identity. It is not conflated with +the smaller released b15 artifacts or another exact-source build. Matching +installed members or reconciliation files are supplementary content evidence +only: they never replace the full wheel, sdist, or npm-tarball digest/SRI for +the specific artifact under test. + +## Required topology + +`scripts/ci/reporting_interop/applicability.json` declares the cells before +execution: + +| Contract | Client | Server | Disposition | +| --- | --- | --- | --- | +| Q1 | candidate Python rc.6 | candidate Python rc.6 | blocking aligned candidate quadrant | +| Q2 | candidate TypeScript rc.6 | candidate Python rc.6 | blocking aligned candidate quadrant | +| Q3 | candidate Python rc.6 | candidate TypeScript rc.6 | blocking aligned candidate quadrant | +| Q4 | candidate TypeScript rc.6 | candidate TypeScript rc.6 | blocking aligned candidate quadrant | +| S1 | previous Python | candidate TypeScript rc.6 | blocking positive skew requirement plus separate negative refusal control | +| S2 | candidate Python rc.6 | previous TypeScript | blocking positive skew requirement plus separate negative refusal control | + +Q1-Q4 are the next-candidate contract, not a selection of pending artifacts. +Rc.47 is the first published TypeScript package in this sequence that pins +3.2.0-rc.6; rc.45/rc.46 pin rc.4, stable 13.1.1 exposes neither the reporting +surface nor these five storyboards, and released Python beta15 pins rc.3. +Consequently no currently selected published pair satisfies S1 or S2. An +actionable version/capability refusal is retained as a separately labelled +negative-skew control only: it cannot satisfy, score, or remove either positive +supported-skew requirement. The existing six-cell runs retain their original +pins and outcomes and are not rebound to this contract. + +Latest-Python and latest-TypeScript canaries are separate and nonblocking. +Every cell owns a fresh process and a fresh PostgreSQL database. PostgreSQL +schemas are insufficient isolation: reporting advisory locks are keyed by +account and are database-wide. + +## Corpus and public surfaces + +The neutral scenario inventory is `scripts/ci/reporting_interop/corpus.json`. +It covers capabilities, retained history, exact reads, receipts, authenticated +account isolation, strict values, pagination, notifications/activity, and the +official-precedence regressions. Applicability is tier-aware and fixed before a +run. + +The five reporting storyboard IDs are fixed, but their step counts are not. +`ts_storyboard_inventory.cjs` executes the exact installed CLI and freezes the +per-storyboard counts for each pin. Rc.42, rc.44, and rc.45 resolve 89 total +steps/84 stateful steps; the historical 64/61 figure belongs only to rc.38. +Inventory is not execution: every resolved step must still run before a cell +can complete. The pin resolver persists every literal phase/step ID, task, +stateful flag, controller scenario, and controller operation, and rejects an +aggregate count that does not match those identities. The development runner +invokes all five IDs separately for every supplied exact TypeScript install, +matches actual task executions back to those identities, and reports every +unexecuted ID rather than counting a requirement-skip wrapper as a storyboard +step. Against the current Core-only +seller, `reporting_core_declaration` executes its one step, while the other +storyboards report the missing public compliance controller instead of being +credited as executed. That is an explicit composition dependency, not a +reduced denominator or a storyboard pass. + +The inventory now derives each storyboard's required tools, capability paths, +controller scenarios, and operations from the exact installed pin's YAML. +The current composition gaps are distinct: + +- `reporting_core` is applicable to the Core seller, but needs a state-backed + controller. Rc.45 exposes `registerTestController` and the flat-store + `reportingCoreLifecycleProbe` hook; its additional + `reliable_reporting_core_integrity_probe` has no turnkey exported adapter + and must be wired through the public open-extension controller surface to + real seller state. The focused adapter preflight now installs hourly Core + configurations and commits zero-row and two-row revisions through the public + producer/store interfaces. A second controlled-clock fixture now implements + the public ledger/status-ingest boundary and is mounted through the documented + `TOOL_INPUT_SHAPE`/`toMcpResponse` MCP extension. Focused real-MCP calls have + driven `prepare`, `advance_time`, `publish_zero_row`, and authoritative status + reads. The installed canonicalizer, literal two-row vector, fixed revision ID, + and both exact-read cursor pages also pass over the focused real-MCP route. + These are adapter preflights, not storyboard execution or PostgreSQL + durability evidence. PostgreSQL snapshots still use their own clock. The + pinned rc.45 producer rejects changing-offset source timezones and plans fixed + millisecond periods. The calendar correction is merged at `f3c7accb` and + published as rc.47, so this is no longer described as an open source defect. + Exact rc.47 byte qualification/selection and literal storyboard execution are + still pending: `probe_scheduler_dst` remains mandatory and uncredited, and no + precomputed boundary is substituted for it. +- `reporting_consumer_status` requires the public `sync_reporting_status` + task, advertised `consumer_status_task`, consumer-scoped ledger state, and + the same lifecycle controller. The controlled fixture now composes the real + status and ingest handlers and has focused public-handler coverage for + wrong-binding rejection, immutable supersession, idempotent replay, and + caller isolation; its basic missing-to-received recovery also passes the + focused real-MCP route. + Several controller operations are implemented but still await complete + storyboard preflight, so this is not an executed consumer-status pass. +- `reliable_reporting_managed_delivery` and + `reliable_reporting_reconciled_billing` remain unsupported on the Core-only + seller, but now have a separate rc.45 PostgreSQL seller/controller + composition. It installs destination authorization and its immutable binding + before the obligation, lets the public managed worker settle a real provider + adapter resource, reads and revokes that resource through the public runtime, + and retains authoritative materialization history. The billing mode adds the + authenticated public receipt handler and two public immutable adjustment + commits. Focused Node 22.12/PostgreSQL 16.4 real-MCP preflights cover rejected + revision evidence, accepted replacement, terminal stale rejection, + accepted/rejected adjustments, repaired adjustment evidence, and + `changes_after` convergence. Rc.45 still exports no turnkey adapters for the + named scenarios, so the harness registers the documented public controller + tool and dispatches only those literal operations. These results prove the + adapter composition, not CLI storyboard execution, a six-cell result, signed + readiness delivery, or controlled-clock behavior. + +The pin-derived inventory also records the public controller surface, so an +absent controller, an unsupported optional tier, and a missing turnkey scenario +adapter remain distinguishable in retained results. The literal operation map +and inspected public source boundaries are retained in +`scripts/ci/reporting_interop/controller-operation-map.json`. + +Python buyer semantics use the installed standalone APIs in `adcp.reporting`, +including `reconcile_reporting_core` and `reconcile_reporting`. There is no +Python `client.reporting` facade in this staged scope. TypeScript rc.45 exposes +both the rich public `reconcileReporting()` path and the distinct Core-only +`reconcileReportingCoreV1()` primitive. Rc.42 lacked the Core primitive, so +its historical rich-reconciler/Core-page mismatch remains a missing-public-API +result rather than a semantic result transferable to rc.45. + +The public TypeScript primary client also lacks reporting status/receipt +methods. The legitimate rich lane uses public `ProtocolClient`, +`unwrapProtocolResponse`, and `reconcileReporting()`; the tracked executable +gap reproduction keeps the primary-facade limitation visible. + +`ts_core_server.cjs` is a real installed-package TypeScript Core seller. It +uses the public PostgreSQL ledger, source producer, status/exact-delivery +handlers, task-capable MCP HTTP server, and bearer verifier. It has passed +account-isolated reads and Python Core reconciliation. Its public inline source +projects scalar evidence only, it does not mount `sync_accounts`, and it must +not be credited for nested-JSON or typed-onboarding scenarios. + +`ts_core_buyer.cjs` retains the raw native rc.45 Core result's +`satisfied`/`health`/`productionStatus`/`reportingRevisionIds` result and a +documented neutral mapping. Historical rc.42 produces an executable +missing-export result. Core availability does not satisfy managed lanes. + +## Current development runner + +`run_foundation_matrix.py` launches the Python and TypeScript public Core +sellers in separate processes and fresh UTF8/C PostgreSQL databases. It runs +both supplied Python artifact routes against both seller languages and +exercises rc.45's native TypeScript Core buyer API over actual HTTP. It also +executes the two +pinned skew controls, every resolved reporting storyboard ID, and a public +Python `WebhookReceiver` with a live loopback revocation-list fetch, issuer +outage, fail-closed stale state, fresh refresh, and recovery. It reaps process +groups, drops databases, freezes installed CLI inventories, hashes retained +evidence, and always emits `acceptance: false` while the declared cells remain +partial. + +`storyboard_orchestration.py` is the separately reviewable process boundary for +the exact five-storyboard denominator. Its default mode validates the Node +executable, registry archive, complete archive-to-install member manifest, +lock/package identity, literal 89/84 inventory, DSN policy, timeouts, and +commands. Execution requires `--execute`; an explicit external test DSN is +preferred. The optional local mode is Linux-only and requires `/proc` plus +pidfds before spawning. Each requested program is held behind a pipe-gated +launcher until the parent owns its pidfd and verifies its new +process-group/session identity. It owns one `postgres` process directly (never +through `pg_ctl`), binds it only to a private Unix socket under its owner-marked +root, and proves `data_directory`, postmaster start time, PID, and process-start +identity before database writes. Shutdown enumerates the exact owned session +and signals every still-matching member through a pidfd rather than an +unguarded numeric process group. If postmaster or descendant death is +unproven, its data root is retained and named in the blocking cleanup error. + +Every seller receives an unpredictable per-run bearer token and startup proof, +writes an exclusive owner-ready record containing its PID and port, and runs in +the identity-checked owned session. The CLI and `initdb` are separately owned; +timeout output is retained and execution and cleanup failures remain distinct. +Database connect, lock, statement, create, and exact-name drop operations are bounded. +Unexpected, duplicate, skipped, or leftover result rows fail the literal +denominator. A complete storyboard result is still not six-cell matrix or +release acceptance. The prior automated `cyberPolicy` reason is unavailable; +these controls define a new reviewable execution path and do not claim to +resolve or bypass that event. + +Run it with installed Python 3.10 artifact environments that contain the +package's `pg` extra: + +```bash +/path/to/python3.10 -I scripts/ci/reporting_interop/run_foundation_matrix.py \ + --python-runtime wheel=/path/to/wheel-venv/bin/python \ + --python-runtime sdist=/path/to/sdist-venv/bin/python \ + --python-artifact wheel=/path/to/candidate.whl \ + --python-artifact sdist=/path/to/candidate.tar.gz \ + --previous-python-runtime /path/to/released-b15-venv/bin/python \ + --previous-python-artifact /path/to/adcp-8.0.0b15-py3-none-any.whl \ + --python-dependency-runtime floor=/path/to/floor-venv/bin/python,2.13.0,2.0.0,wheel \ + --python-dependency-runtime current=/path/to/current-venv/bin/python,2.13.5,2.2.0,wheel \ + --node-runtime /path/to/node-22.12.0 \ + --typescript-install candidate=/path/to/future-selected-exact-rc47-npm-install \ + --typescript-install previous=/path/to/exact-rc41-npm-install \ + --typescript-archive candidate=/path/to/future-selected-sdk-14.0.0-rc.47.tgz \ + --typescript-archive previous=/path/to/sdk-14.0.0-rc.41.tgz \ + --pg-admin-url postgresql://USER:PASSWORD@HOST:PORT/postgres \ + --output .context/reporting-interop/foundation-run +``` + +The focused storyboard boundary receives the matching archive again as +`--typescript-tarball`, plus either `--external-pg-admin-dsn` or +`--local-pg-bin`. Its plan-only default starts nothing. The outer runner passes +`--execute` only in a separately authorized bounded run. + +The previous released Python wheel keeps its own SHA-256 gate and derives its +installed SDK member count/digest from those exact wheel bytes. Candidate +routes retain the independently selected candidate-member manifest. An +actionable previous-version refusal remains a negative-only control and cannot +inherit candidate content expectations or acceptance credit. + +Foundation accounting does not trust a returned cell ID. Required credit also +requires the declared contract ID, exact selected protocol/artifacts, positive +semantic polarity (or a genuinely selected positive skew pair), and a fresh +seller/database identity for that one cell. Artifact credit compares a +contract-bound aggregate identity digest rather than trusting an asserted +"selected" boolean, and seller/database identities must be unique across +credited cells. The retained rc.4 Core controls +are named `supplemental_shared_rc4__*`; the released-Python refusal is named +`negative_skew_refusal__*`. They remain visible evidence but leave Q1-Q4 and +the positive S1/S2 requirements unexecuted. Latest canaries have separate +executed/missing accounting and never confer blocking acceptance. + +The older `scripts/ci/reporting_interop_matrix.py validate-results` command +still validates its historical four-cell preparation shape, evidence hashes, +and asserted isolation fields. Its command result is now explicitly +`legacy_result_shape_validated_nonaccepting`; synthetic assertions and process +IDs cannot promote the current matrix. Likewise, the offline TypeScript +primary-facade probe reports method-surface availability only and always keeps +`semantic_lane_complete: false`. + +The managed/reconciled real-MCP preflight uses the same admitted +`{account_id, sandbox: true}` selector for controller, status, exact-history, +and receipt calls. Separate missing- and false-sandbox probes must be rejected; +the trusted seller guard is not weakened to accommodate a harness selector. + +## Review and scanner lineage + +Independent source review covered the 811-line orchestration boundary at exact +head `6df004940461a58d28591bad67720d5e5e8440d3`; it did not attest the full +61-file historical scaffold. GitGuardian check `108042611489` remains an +authentic failure with incident `37600271`, pointing to the deliberately +synthetic required-reject `kv-assignment` in historical commit `8cefe4629`. +That record is consumed only by the two seller-side resource-location guard +probes and contains no authentication consumer. Its fixture bytes and finding +semantics remain unchanged: no ignore, history rewrite, rule weakening, +obfuscation, alert dismissal, or substitution is made here. + +The current result is deliberately `incomplete`; it cannot be wired into a +release acceptance gate yet. Candidate Python currently reconciles the rc.41 +Core seller over real MCP HTTP. Released Python b15 rejects the explicit rc.4 +candidate server request during public client construction with an actionable +supported-version error and no transport mutation. Those are concrete skew +scenario results, but neither completes the remaining applicable scenario set +for its cell. The frozen release entrypoint must additionally run Managed +Delivery/Reconciled Billing semantics, server-only durability/security +scenarios, complete resolved storyboard steps through the required controller, +and visible canaries. + +## Current blocking findings + +- Historical rc.42 official precedence remains red with no exemption: all 17 adapted + inputs validate against both public rc.4 validators, but only five controls + meet required semantics. Four unlinked histories return + `AMBIGUOUS_REVISION_CHAIN`; eight official-without-materialization histories, + including linked variants, throw `LEDGER_GRAPH_INTEGRITY_FAILED`. + The protocol's byte-identical normative rc.4 reconciliation fixture contains + only a single official revision and no supersession field, retained + snapshot/official history, multi-leaf chain, or artifact-free official. Its + green conformance therefore does not cover either failing selection shape; + the signed protocol bundle stays unchanged. Release protection comes from + the shipped SDK's required-correct 17-case tests plus this separately pinned + neutral 17-case harness corpus. Protocol-owned vector enrichment is a + distinct future contribution, not a prerequisite or claim of current rc.4 + coverage. + The separately identified rc.45 development candidate passes all 17 + required-correct cases through both public buyer APIs with all inputs valid, + no caller mutation, and no inspection/receipt effects. That is candidate + package evidence only; it neither rewrites rc.42 nor closes the HTTP, + storyboard, skew, Python, or integrated-release rows. +- Historical rc.42 has no public `reconcileReportingCoreV1` or + `@adcp/sdk/client/core` export. Its rich reconciler correctly remains a + different Managed Delivery API and cannot close a Core buyer cell. Rc.45 + includes the Core API and must retain its native result shape; availability + alone does not close a real HTTP Core cell or any managed lane. +- Accepted Python typed `sync_accounts` currently serializes an explicit + `media_buy_ids` reporting scope together with default + `all_media_buys: true`. The real production mount rejects the mutually + exclusive pair before onboarding. A manually seeded Core ledger does not + close that production-client lane. +- Accepted Python typed `get_media_buy_delivery` currently serializes both + aggregate breakdown flags as false on an exact-revision request. The rc.4 + schema forbids either field in that mode, so a real produced revision is + rejected before its first content page. A lower-level exact-read success is + not typed-client evidence. +- The isolated adoption head's metadata still permits Pydantic 2.12, whose + public generated-type imports fail. That historical red is already + dispositioned by merged main-only #1190, which sets the reviewed floor to + Pydantic 2.13.0. Development controls run exact CPython 3.10/Pydantic + 2.13.0/MCP 2.0.0 and a current 2.13.5/2.2.0 lane from locked dependency + snapshots; their successful imports and preserved PY-PUBLIC-001/002 reds do + not transfer to integrated main. Final artifacts must independently expose + metadata excluding 2.12 and pass imports/models/reporting cases at the floor. +- A bounded installed-artifact sweep of 209 request/default cases per Python + build route found no additional selector/default defect: 159 valid variants + remained valid, 12 scoped-configuration variants reproduced only the typed + onboarding defect above, five exact-read variants reproduced only the typed + delivery defect above, and 33 invalid controls stayed invalid. This narrows + correction scope but is request-serialization evidence, not production or + semantic-quadrant acceptance. +- The Managed Delivery TypeScript seller/controller has focused public-runtime + and real-MCP preflights, but its CLI storyboard execution and the managed + Python composition, normalized transcript comparator, signed retry/activity + execution, skew cells, and aggregate fail-closed validator are not yet + frozen. +- Candidate Python still silently aliases some distinct wire-decoded integral + filter values above 2^53 after Decimal-to-float conversion; that remains a + blocking real-boundary row. The pre-existing `ctx_metadata` suffix screen is + separately retained as a nonblocking documentation/hardening row: its + guarantee must say best-effort defense in depth, not fail-closed. +- The TypeScript seller's resource-location persistence guard at the historical + rc.42 pin admits + bare JWTs, cloud key identifiers, and percent-encoded presigned forms that + the Python seller rejects. The Python buyer does not re-validate these + locations, and Python's provider-native-only rule is deliberately stricter, + so the complete 17-case in-process comparison is retained as one visible + nonblocking TypeScript hardening advisory, not seven equivalent defects or a + quadrant result. Future hardening needs a compatibility/false-positive + budget and bounded decoding that preserves legitimate native locations; a + blanket parse-failure rejection is not assigned. This advisory never waives + a red result if an actual public path emits a synthetic secret or signed URL. +- The repository's existing CI does not pin rc.45: its TypeScript jobs resolve + floating `latest` or legacy dist-tags. Those greens are neither immutable + candidate evidence nor a substitute for the harness's exact npm lock, + integrity and Node-floor cells. +- T-1 remains a visible nonblocking transport-metadata defect: the independently + reproduced TypeScript path omits `responseHeaders`, affecting all diagnostic + response headers, at both the declared Undici 6.28.0 floor and 6.28.1. This + row is retained separately from reporting semantics and is not inferred + green from rc.45 publication. T-2 remains separate recurring specialism-test + timeout fragility; no retry or test-budget expansion is hidden in this + harness. +- The protocol-owned rc.4 webhook-signing corpus is byte-identical across the + installed SDKs. Historical 1f953 evidence remains 27/29 Python and 28/29 + TypeScript. The selected PR1208 Python build now matches 28/29: mixed-base64 + vector 021 is classified as normative `header_malformed`, while both bare + verifier calls still leave vector 019's receiver-runner revocation-staleness + state unconfigured. Python's public `revocation_list` option passes a deterministic + fresh/stale in-process control on both artifact routes, proving the state is + expressible without a hidden option. The separate live receiver lane now + exercises stale-fetch outage and refresh recovery. The harness's separate 13/13 + real-HTTP replay/signing controls do not replace these 29 normative vectors. +- A separate public signer/verifier 2x2 is asymmetric on the retained + historical installed inputs: Python's webhook signer emitted padded standard + Base64, which Python accepts but rc.42 + TypeScript rejects as `webhook_signature_header_malformed`; TypeScript emits + unpadded Base64URL, which both verifiers accept. This is a blocking + Python-to-TypeScript callback semantic red distinct from mixed-alphabet + verifier classification and stale-revocation receiver state. Immutable rc.4 + protocol source requires unpadded Base64URL for the webhook Signature token, + so current Python emission is the defect; Python decoder tolerance is not + conformance. Content-Digest remains separately bounded because source wording + and immutable vectors differ, and its bytes are not recoded by this finding. + Historical same-language success and the 13-vector control do not close this + direction. The selected PR1208 build instead emits the required unpadded + Base64URL Signature and is accepted by the exact rc.45 TypeScript verifier; + both artifact routes remain required in the development result. +- The real public receiver composition now exercises live revocation fetch, + stale issuer outage and refresh recovery over raw loopback HTTP. Fresh and + recovered callbacks are accepted and handled; the stale state fails closed. + On the historical Python artifacts it escaped the verifier wrapper as an + operational `RevocationListFreshnessError`, without a signature code or + failed-step attribute, rather than the normative vector-019 + `webhook_signature_revocation_stale` classification. The harness retains + that exact red and its HTTP fixture deliberately maps the otherwise-uncaught + exception to 503; 503 is harness behavior, not an SDK `WebhookOutcome`. + Normative inner-code matching and the SDK's current 401/`WWW-Authenticate` + receiver convention are asserted separately. The blocking invocation exits + nonzero for the historical red; an explicit diagnostic reproducer can exit + zero only when that exact red occurs and reports `blocking_acceptance: false`. + It fails as stale if a corrected artifact produces the normative result. + On both selected PR1208 artifact routes, the same live composition returns + the normative inner code and the receiver's current 401/`WWW-Authenticate` + mapping, then recovers after refresh. This live cell configures + `replay_store=None`, so it does not claim replay-persistence evidence. + This is receiver-composition evidence, not polling-service deployment, + cross-language signer interoperability, reporting retry/account activity, or + a substitute for the 29-vector corpus. +- A late-account real production run repeatedly published a later account's + revision but did not materialize it after an earlier account completed. An + independent real-PostgreSQL store probe confirmed that retaining the + pre-update `served_at` continuation can chase a continuously due first + account and never wrap to the late `-infinity` row under the reproduced + preconditions. This mechanism is sufficient for that red, but does not + imply permanent starvation after a due gap or exclude every unrelated + defect. A separate read-only public-loop observation recorded 26/26 completed + claims during a 45-second late-account read window sampling and serving only + the continuously due first account; all cursors remained on that account and + the late row remained at `-infinity`. The distinct runtime correction and + bounded public fairness/restart/concurrency regressions remain open. +- PY-FINALITY-001 is a separate Python producer-time blocker. A valid source + can observe and finalize during acquisition after worker dispatch, while the + historical producer records its pre-fetch worker clock as + `revision.created_at`; this can make `finalized_at > created_at`, which the + buyer correctly rejects as `FINALITY_EVIDENCE_INVALID`. Historical memory + producer diagnostics and the b203 wire inversion remain distinct evidence: + neither is an HTTP/PostgreSQL acceptance result. The eventual selected + artifact must preserve source evidence and exercise real typed MCP status, + exact read, reconciliation, and replay/restart, while future/skew/regressing + clock negatives still fail before publication. This row applies only to + Python producer-capable server cells and does not infer a TypeScript defect. + +These are preparation/qualification results on the accepted stacked head. +They must be repeated against the actual integrated main tree; the conflict-free +prospective merge tree is not execution evidence. + +## Development validation + +```bash +uv run pytest -q \ + tests/test_reporting_interop_matrix.py \ + tests/test_reporting_interop_corpus.py + +node --check scripts/ci/reporting_interop/ts_package_probe.cjs +node --check scripts/ci/reporting_interop/ts_mcp_buyer.cjs +node --check scripts/ci/reporting_interop/ts_primary_facade_gap.cjs +node --check scripts/ci/reporting_interop/ts_core_server.cjs +node --check scripts/ci/reporting_interop/ts_core_buyer.cjs +``` + +The older `scripts/ci/reporting_interop_matrix.py` remains the immutable +registry/protocol evidence verifier from the preparation commit. Its original +version cross-product is not the language-quadrant execution model and must not +be presented as matrix acceptance. diff --git a/scripts/ci/reporting_interop/applicability.json b/scripts/ci/reporting_interop/applicability.json new file mode 100644 index 000000000..5abeb3533 --- /dev/null +++ b/scripts/ci/reporting_interop/applicability.json @@ -0,0 +1,155 @@ +{ + "schema_version": 1, + "corpus": "corpus.json", + "dispositions": { + "blocking_required": "Must pass for this blocking cell.", + "visible_nonblocking": "Must execute and remain visible, but cannot block or establish acceptance.", + "actionable_unsupported": "Must fail before mutation with a specific capability or schema error.", + "not_applicable": "Outside the declared client-language or server-foundation scope.", + "deferred_open": "Explicitly outside this staged release and still open." + }, + "cells": [ + { + "id": "candidate_py_client__candidate_py_server", + "contract_id": "Q1", + "family": "candidate_quadrant", + "blocking": true, + "protocol_contract": {"required": "3.2.0-rc.6", "alignment": "exact", "selected_artifacts": false}, + "client": {"language": "python", "package_role": "candidate"}, + "server": {"language": "python", "package_role": "candidate"} + }, + { + "id": "candidate_ts_client__candidate_py_server", + "contract_id": "Q2", + "family": "candidate_quadrant", + "blocking": true, + "protocol_contract": {"required": "3.2.0-rc.6", "alignment": "exact", "selected_artifacts": false}, + "client": {"language": "typescript", "package_role": "candidate"}, + "server": {"language": "python", "package_role": "candidate"} + }, + { + "id": "candidate_py_client__candidate_ts_server", + "contract_id": "Q3", + "family": "candidate_quadrant", + "blocking": true, + "protocol_contract": {"required": "3.2.0-rc.6", "alignment": "exact", "selected_artifacts": false}, + "client": {"language": "python", "package_role": "candidate"}, + "server": {"language": "typescript", "package_role": "candidate"} + }, + { + "id": "candidate_ts_client__candidate_ts_server", + "contract_id": "Q4", + "family": "candidate_quadrant", + "blocking": true, + "protocol_contract": {"required": "3.2.0-rc.6", "alignment": "exact", "selected_artifacts": false}, + "client": {"language": "typescript", "package_role": "candidate"}, + "server": {"language": "typescript", "package_role": "candidate"} + }, + { + "id": "previous_py_client__candidate_ts_server", + "contract_id": "S1", + "family": "supported_skew", + "blocking": true, + "positive_requirement": {"status": "unmet_no_published_compatible_pair", "blocking": true}, + "negative_skew_control": {"required": true, "credit": "refusal_only_never_positive_acceptance"}, + "client": {"language": "python", "package_role": "previous_released"}, + "server": {"language": "typescript", "package_role": "candidate"} + }, + { + "id": "candidate_py_client__previous_ts_server", + "contract_id": "S2", + "family": "supported_skew", + "blocking": true, + "positive_requirement": {"status": "unmet_no_published_compatible_pair", "blocking": true}, + "negative_skew_control": {"required": true, "credit": "refusal_only_never_positive_acceptance"}, + "client": {"language": "python", "package_role": "candidate"}, + "server": {"language": "typescript", "package_role": "previous_compatible"} + }, + { + "id": "latest_py_client__candidate_ts_server", + "family": "latest_canary", + "blocking": false, + "client": {"language": "python", "package_role": "latest_registry"}, + "server": {"language": "typescript", "package_role": "candidate"} + }, + { + "id": "latest_ts_client__candidate_py_server", + "family": "latest_canary", + "blocking": false, + "client": {"language": "typescript", "package_role": "latest_registry"}, + "server": {"language": "python", "package_role": "candidate"} + } + ], + "defaults": { + "candidate_quadrant": "blocking_required", + "supported_skew": "blocking_required", + "latest_canary": "visible_nonblocking" + }, + "overrides": [ + { + "scenario": "foundation.reconciliation.python_standalone", + "by_client_language": {"python": "default", "typescript": "not_applicable"} + }, + { + "scenario": "foundation.reconciliation.typescript_public", + "by_client_language": {"python": "not_applicable", "typescript": "default"}, + "requires_server_tier": "managed_delivery" + }, + { + "scenario": "foundation.webhook.signed_retry_dedup", + "by_server_scope": {"python_reporting_foundation": "default", "other": "not_applicable"} + }, + { + "scenario": "foundation.activity.durable_account_feed", + "by_server_scope": {"python_reporting_foundation": "default", "other": "not_applicable"} + }, + { + "scenario": "foundation.runtime.late_account_materialization_fairness", + "by_server_scope": {"python_reporting_foundation": "default", "other": "not_applicable"} + }, + { + "scenario": "foundation.runtime.producer_finality_temporal_order", + "by_server_scope": {"python_reporting_foundation": "default", "other": "not_applicable"} + }, + { + "scenario": "foundation.security.ctx_metadata_credentials", + "all": "visible_nonblocking" + }, + { + "scenario": "foundation.security.resource_location_credential_screen", + "all": "visible_nonblocking" + }, + { + "scenario": "foundation.transport.response_headers_metadata", + "all": "visible_nonblocking" + }, + { + "scenario": "foundation.skew.actionable_unsupported", + "by_family": { + "candidate_quadrant": "not_applicable", + "supported_skew": "actionable_unsupported", + "latest_canary": "visible_nonblocking" + } + }, + { + "scenario": "deferred.1172.service_composition", + "all": "deferred_open" + }, + { + "scenario": "deferred.1172.client_reporting_facade", + "all": "deferred_open" + }, + { + "scenario": "deferred.1182.provisional_reread", + "all": "deferred_open" + } + ], + "execution_contract": { + "fresh_processes_per_cell": true, + "isolated_database_per_cell": true, + "real_mcp_http": true, + "postgresql": "required_for_durable_seller_scenarios", + "installed_packages_only": true, + "latest_canaries_nonblocking": true + } +} diff --git a/scripts/ci/reporting_interop/controller-operation-map.json b/scripts/ci/reporting_interop/controller-operation-map.json new file mode 100644 index 000000000..0edad8f9e --- /dev/null +++ b/scripts/ci/reporting_interop/controller-operation-map.json @@ -0,0 +1,276 @@ +{ + "schema_version": 1, + "typescript_pin": "14.0.0-rc.45", + "protocol": "3.2.0-rc.4", + "source": "installed rc.45 universal storyboard YAML plus public package declarations", + "scenarios": [ + { + "scenario": "reporting_core_lifecycle_probe", + "operations": [ + { + "operation": "prepare", + "step_ids": [ + "prepare_waiting_obligation", + "prepare_nonempty_core", + "prepare_omission_fixture", + "prepare_revision_fixture", + "prepare_silence_fixture", + "prepare_mismatch_fixture" + ], + "transition": "Install an account-scoped Core configuration and expose its first elapsed obligation before any revision.", + "public_api": [ + "createReportingProducer", + "ReportingProducerV1.installConfiguration", + "ReportingProducerV1.planObligations", + "PostgresReportingLedgerStore.listObligations" + ], + "implementation": "controlled_clock_public_store_and_real_mcp_preflight_passed", + "integration_boundary": "The harness-controlled store implements the exported ReportingConsumerStatusLedgerStore/read boundary and is distinct from the PostgresReportingLedgerStore durability lane. PostgreSQL still has no clock injection.", + "source_boundary": [ + "dist/lib/reporting/ledger/postgres.d.ts:101-116", + "dist/lib/reporting/ledger/postgres.js:1377-1385", + "dist/lib/reporting/ledger/handler.js:66-96" + ] + }, + { + "operation": "advance_time", + "step_ids": [ + "advance_beyond_sla", + "advance_beyond_recovery", + "advance_omission_beyond_expected", + "advance_revision_beyond_expected", + "advance_mismatch_beyond_recovery" + ], + "transition": "Move the same prepared obligation across expected_at and recovery_deadline without replacing its schedule identity.", + "public_api": [ + "createReportingStatusHandler", + "PostgresReportingLedgerStore.createSnapshot" + ], + "implementation": "controlled_clock_public_store_and_real_mcp_preflight_passed", + "integration_boundary": "The public status handler consumes the controlled store snapshot clock. No immutable deadline or handler response is rewritten, and this is not PostgreSQL clock evidence.", + "source_boundary": [ + "dist/lib/reporting/ledger/handler.d.ts:4-16", + "dist/lib/reporting/ledger/postgres.js:1383" + ] + }, + { + "operation": "publish_zero_row", + "step_ids": [ + "publish_zero_row", + "publish_recovered_revision", + "publish_mismatch_revision" + ], + "transition": "Return an explicit empty source result and commit one immutable snapshot revision for the prepared obligation.", + "public_api": [ + "ReportingSourceExecutorV1", + "buildReportingSourceManifestV1", + "completedReportingSourceResponseV1", + "ReportingProducerV1.runWorker", + "PostgresReportingLedgerStore.listRevisions" + ], + "implementation": "postgres_producer_and_controlled_clock_real_mcp_preflights_passed" + }, + { + "operation": "publish_nonempty", + "step_ids": ["publish_nonempty_core"], + "transition": "Commit the installed two-row vector and retain its exact revision metadata and paged rows.", + "public_api": [ + "ReportingSourceExecutorV1", + "buildReportingSourceManifestV1", + "completedReportingSourceResponseV1", + "ReportingProducerV1.runWorker", + "createReportingDeliveryHandler", + "createReportingStatusHandler" + ], + "implementation": "controlled_store_literal_vector_real_mcp_preflight_passed", + "integration_boundary": "The controlled fixture recomputes the literal RFC 8785 binding with the installed public canonicalizer and exposes both exact cursor pages through createReportingDeliveryHandler. The earlier producer-derived rrev_* result remains a distinct PostgreSQL preflight and is not relabeled.", + "source_boundary": [ + "dist/lib/reporting/ledger/producer.js:554-571", + "dist/lib/reporting/ledger/postgres.d.ts:141-144" + ] + }, + { + "operation": "omit_obligation", + "step_ids": ["omit_expected_obligation"], + "transition": "Keep a derivable elapsed period absent while returning the independently expected configuration and period identity.", + "public_api": [ + "ReportingProducerV1.installConfiguration", + "PostgresReportingLedgerStore.listObligations" + ], + "implementation": "controlled_fixture_adapter_implemented_preflight_pending" + }, + { + "operation": "restate_after_received", + "step_ids": ["restate_received_revision", "advance_past_restatement_grace"], + "transition": "Publish one convergent superseding snapshot bound to the buyer's received leaf, then project inside or beyond its grace deadline.", + "public_api": [ + "createInlineReportingSourceExecutor", + "ReportingProducerV1.runWorker", + "PostgresReportingLedgerStore.listRevisions", + "createSyncReportingStatusHandler" + ], + "implementation": "controlled_fixture_adapter_implemented_preflight_pending", + "integration_boundary": "The controlled store preserves immutable supersession and advances only its snapshot clock; real sync_reporting_status remains the wire operation." + }, + { + "operation": "advance_past_status_deadline", + "step_ids": ["advance_past_status_deadline"], + "transition": "Cross expected_at plus the advertised recovery window without recording consumer status.", + "public_api": ["createReportingStatusHandler"], + "implementation": "controlled_fixture_adapter_implemented_preflight_pending" + }, + { + "operation": "advance_past_escalation", + "step_ids": ["advance_past_mismatch_escalation"], + "transition": "Cross the durable mismatch issue's advertised escalation deadline while retaining issue identity.", + "public_api": [ + "createReportingStatusHandler", + "PostgresReportingLedgerStore.listIssues" + ], + "implementation": "controlled_fixture_adapter_implemented_preflight_pending" + } + ] + }, + { + "scenario": "reliable_reporting_core_integrity_probe", + "operations": [ + { + "operation": "probe_scheduler_dst", + "step_ids": ["run_real_source_calendar_dst_scheduler"], + "transition": "Plan real source_timezone P1D obligations across New York spring-forward and fall-back boundaries.", + "public_api": [ + "reportingScheduleOriginV1", + "ReportingProducerV1.installConfiguration", + "ReportingProducerV1.planObligations" + ], + "implementation": "rc47_development_selected_execution_pending", + "rc45_limitation": "The pinned rc.45 public source module exposes the interval-zero origin and UTC-offset probes, but no civil-time next-boundary generator. Its ReportingProducerV1 plans fixed periodMilliseconds intervals and rejects source_timezone configurations when reportingUtcOffsetChangesV1 detects a DST transition, so rc.45 cannot produce the storyboard's 23-hour and 25-hour New York days.", + "current_source_disposition": "The frozen calendar correction is independently source-approved and exact rc.47 registry bytes are selected for developmental Q1-Q4 qualification. The harness has not locked, installed, or executed rc.47, so the mandatory step remains unexecuted rather than an open source defect.", + "source_boundary": [ + "dist/lib/reporting/source/source.d.ts:1549-1573", + "dist/lib/reporting/ledger/producer.js:940-979", + "dist/lib/reporting/ledger/producer.js:365-410" + ] + }, + { + "operation": "prepare", + "step_ids": ["prepare_core_integrity"], + "transition": "Install a source_timezone official configuration and persist its pre-publication checkpoint.", + "public_api": [ + "createReportingProducer", + "ReportingProducerV1.installConfiguration", + "ReportingProducerV1.planObligations", + "createReportingStatusHandler" + ], + "implementation": "planned_public_adapter" + }, + { + "operation": "publish_official_adjustment", + "step_ids": ["publish_official_and_adjustment_out_of_order"], + "transition": "Commit an official revision, a later immutable adjustment, and capture adjustment-first duplicate notification order for checkpoint repair.", + "public_api": [ + "createInlineReportingSourceExecutor", + "ReportingProducerV1.runWorker", + "PostgresReportingLedgerStore.listAdjustments", + "ReportingLedgerSubscriberV1" + ], + "implementation": "planned_public_adapter" + } + ] + }, + { + "scenario": "reliable_reporting_managed_delivery_probe", + "operations": [ + { + "operation": "prepare", + "step_ids": ["prepare_managed_materialization"], + "transition": "Install a destination binding, publish a revision, deliver and verify one retained resource.", + "public_api": [ + "PostgresReportingLedgerStore({managedDelivery:true})", + "PostgresReportingManagedDeliveryStore", + "createReportingManagedDeliveryRuntime", + "runManagedDeliveryWorker" + ], + "implementation": "postgres_public_runtime_and_real_mcp_preflight_passed", + "artifact": "scripts/ci/reporting_interop/ts_managed_reporting_fixture.cjs", + "callable": "createManagedReportingFixture(..., {mode:'managed'}).controller('reliable_reporting_managed_delivery_probe','prepare')", + "observable": "get_reporting_status revision view returns the exact available materialization with verification and resource expiry", + "integration_boundary": "Binding authorization and installation precede the obligation. The public runtime, not the controller, plans and settles the provider materialization." + }, + { + "operation": "suppress_readiness", + "step_ids": ["suppress_managed_readiness"], + "transition": "Suppress the optional readiness subscriber while retaining authoritative polling state.", + "public_api": ["ReportingLedgerSubscriberV1"], + "implementation": "fixture_notification_gate_and_real_mcp_polling_recovery_preflight_passed", + "artifact": "scripts/ci/reporting_interop/ts_managed_reporting_fixture.cjs", + "callable": "controller(..., 'suppress_readiness') followed by runtime.getReportingStatus", + "integration_boundary": "No readiness subscriber is installed, so polling is authoritative. This suppression earns no signed-webhook credit." + }, + { + "operation": "advance_within_retention", + "step_ids": ["prove_resource_retention"], + "transition": "Read the original resource before its committed expiry.", + "public_api": [ + "readManagedReportingResource", + "ReportingManagedDeliveryStore.getReadableResource" + ], + "implementation": "public_runtime_resource_read_real_mcp_preflight_passed", + "artifact": "scripts/ci/reporting_interop/ts_managed_reporting_fixture.cjs", + "callable": "ReportingManagedDeliveryRuntimeV1.readResource", + "observable": "the provider returns the exact original bytes while the durable grant and resource expiry remain current" + }, + { + "operation": "revoke_access", + "step_ids": ["revoke_managed_access"], + "transition": "Revoke the destination generation, complete cleanup inside the advertised bound, and retain immutable ledger history.", + "public_api": [ + "ReportingManagedDeliveryStore.revokeDestination", + "ReportingManagedDeliveryStore.claimRevocation", + "ReportingManagedDeliveryStore.completeRevocation" + ], + "implementation": "durable_revoke_provider_cleanup_and_real_mcp_preflight_passed", + "artifact": "scripts/ci/reporting_interop/ts_managed_reporting_fixture.cjs", + "callable": "PostgresReportingManagedDeliveryStore.revokeDestination then ReportingManagedDeliveryRuntimeV1.runWorker", + "observable": "public resource read is denied, provider revoke ran, and immutable materialization metadata remains in status" + } + ] + }, + { + "scenario": "reliable_reporting_reconciled_billing_probe", + "operations": [ + { + "operation": "prepare", + "step_ids": ["prepare_reconciled_billing"], + "transition": "Publish an official billing revision, verified materialization, and receipt-ready consumer scope.", + "public_api": [ + "createReportingManagedDeliveryRuntime", + "runManagedDeliveryWorker", + "createSyncReportingReceiptsHandler" + ], + "implementation": "postgres_public_runtime_receipt_scope_and_real_mcp_preflight_passed", + "artifact": "scripts/ci/reporting_interop/ts_managed_reporting_fixture.cjs", + "callable": "createManagedReportingFixture(..., {mode:'billing'}).controller('reliable_reporting_reconciled_billing_probe','prepare')", + "observable": "period status begins action_required/RECEIPT_REQUIRED; rejected then accepted superseding receipts are durable and a stale terminal supersession fails", + "integration_boundary": "The runtime advertises reconciled_billing only after adopting the full policy and authenticated consumer resolver." + }, + { + "operation": "publish_adjustment", + "step_ids": ["publish_reconciled_adjustments"], + "transition": "Commit two immutable adjustments targeting the official revision for accepted and disputed receipt paths.", + "public_api": [ + "PostgresReportingLedgerStore.commitAdjustment", + "reportingCanonicalAdjustmentSha256V1", + "PostgresReportingLedgerStore.listAdjustments", + "createSyncReportingReceiptsHandler" + ], + "implementation": "two_public_adjustment_commits_receipt_repair_and_real_mcp_preflight_passed", + "artifact": "scripts/ci/reporting_interop/ts_managed_reporting_fixture.cjs", + "callable": "controller(..., 'publish_adjustment') then ReportingManagedDeliveryRuntimeV1.syncReportingReceipts", + "observable": "two adjustments reopen reconciliation; accepted/rejected outcomes, replacement, terminal stale rejection, append-only count, and changes_after repair are visible", + "integration_boundary": "The controller commits only immutable correction facts. Every receipt outcome traverses the installed public receipt handler." + } + ] + } + ] +} diff --git a/scripts/ci/reporting_interop/core_scenario.json b/scripts/ci/reporting_interop/core_scenario.json new file mode 100644 index 000000000..4feb74612 --- /dev/null +++ b/scripts/ci/reporting_interop/core_scenario.json @@ -0,0 +1,31 @@ +{ + "schema_version": 1, + "scenario_id": "foundation.reconciliation.python_standalone", + "request": { + "account": {"account_id": "interop-account-a"}, + "view": "periods", + "period": { + "start": "2026-08-01T00:00:00Z", + "end": "2026-08-02T00:00:00Z" + } + }, + "expected_periods": [ + { + "deliveryConfigId": "shared-daily-key", + "deliveryConfigVersion": 1, + "reportDefinitionId": "interop-core-v1", + "feedPurpose": "analytics", + "reportingProfile": "interop-core-v1", + "mediaBuyIds": ["media-buy-a"], + "periodStart": "2026-08-01T00:00:00Z", + "periodEnd": "2026-08-02T00:00:00Z" + } + ], + "now": "2026-09-03T00:00:00Z", + "expected_outcome": { + "definitive": true, + "inspection_calls": 0, + "obligation_count": 1, + "reasons_absent": [] + } +} diff --git a/scripts/ci/reporting_interop/corpus.json b/scripts/ci/reporting_interop/corpus.json new file mode 100644 index 000000000..0a05cfbbe --- /dev/null +++ b/scripts/ci/reporting_interop/corpus.json @@ -0,0 +1,254 @@ +{ + "schema_version": 1, + "scope": "staged_reporting_foundations", + "protocol_candidate": "3.2.0-rc.4", + "rules": { + "transport": "real_separate_process_mcp_http", + "seller_durability": "real_postgresql", + "result_comparison": "normalized_redacted_public_semantics", + "manufactured_success": false, + "raw_protocol_oracle_counts_as_semantic_pass": false, + "expected_defect_exemptions": false + }, + "scenarios": [ + { + "id": "foundation.capabilities.fail_closed", + "area": "capabilities", + "stage": "foundation", + "expectation": "Advertise Core, Managed Delivery, and Reconciled Billing only when the installed public server composition supports each tier; absence and unsupported schema combinations are explicit." + }, + { + "id": "foundation.status.complete_history", + "area": "status_history", + "stage": "foundation", + "expectation": "Return complete retained obligation, revision, materialization, receipt, issue, count, scope, and continuation semantics without private-structure comparison." + }, + { + "id": "foundation.status.exact_revision_read", + "area": "status_history", + "stage": "foundation", + "expectation": "An exact public revision read returns the requested immutable revision or an actionable not-found or unsupported result without substituting another revision." + }, + { + "id": "foundation.delivery.typed_exact_revision_pagination", + "area": "revision_content", + "stage": "foundation", + "expectation": "The public typed delivery client reads an exact reporting_revision_id and every content page without injecting aggregate-only breakdown defaults; explicit forbidden fields remain rejected and revision identity, row counts, digests, zero rows, and more-than-500-row traversal remain exact." + }, + { + "id": "foundation.configuration.explicit_scope_roundtrip", + "area": "configuration", + "stage": "foundation", + "expectation": "A typed sync_accounts request that selects explicit media_buy_ids emits exactly that mutually exclusive scope, never also a default all_media_buys flag, and reaches authenticated production onboarding over MCP without manual wire repair." + }, + { + "id": "foundation.receipts.idempotent_submission", + "area": "receipts", + "stage": "foundation", + "expectation": "Accepted, rejected, and replacement receipt submissions are account-bound, durable, replay-idempotent, and visible in a subsequent public status read." + }, + { + "id": "foundation.reconciliation.python_standalone", + "area": "semantic_reconciliation", + "stage": "foundation", + "client_language": "python", + "public_entrypoints": [ + "adcp.reporting.load_reporting_ledger", + "adcp.reporting.reconcile_reporting", + "adcp.reporting.reconcile_reporting_core", + "adcp.reporting.plan_consumer_statuses", + "adcp.reporting.post_consumer_statuses" + ], + "expectation": "Exercise the installed standalone Python buyer functions over public client calls; do not claim a client.reporting facade." + }, + { + "id": "foundation.reconciliation.typescript_public", + "area": "semantic_reconciliation", + "stage": "foundation", + "client_language": "typescript", + "requires_server_tier": "managed_delivery", + "public_entrypoints": [ + "@adcp/sdk.reconcileReporting", + "@adcp/sdk.evaluateReportingLedger", + "@adcp/sdk.buildReportingReceipt" + ], + "expectation": "Exercise installed public TypeScript reconciler primitives against verified destination materialization evidence; the package has no Core-only reconcileReporting counterpart, and a raw JSON-RPC call is not a semantic reconciliation pass." + }, + { + "id": "foundation.auth.opaque_https_principals", + "area": "auth", + "stage": "foundation", + "expectation": "Opaque and HTTPS consumer principals resolve only in the authenticated account context; missing, invalid, or cross-account credentials fail closed." + }, + { + "id": "foundation.isolation.two_accounts_reused_keys", + "area": "isolation", + "stage": "foundation", + "expectation": "Two accounts may reuse configuration and caller keys without leaking status, receipts, canonical consumers, activity, or continuation state." + }, + { + "id": "foundation.values.strict_numbers_json", + "area": "value_integrity", + "stage": "foundation", + "expectation": "Fractions and nested bool, number, string, null, array, and object distinctions survive; transport-equivalent 1 and 1.0 bind where specified, while bool substitution, value changes, and unsafe integer precision loss fail closed without mutation." + }, + { + "id": "foundation.values.transport_large_integer_binding", + "area": "value_integrity", + "stage": "foundation", + "expectation": "Wire decimal/exponent spellings above the JSON safe-integer range never collapse distinct vendor/context filter values into one cursor or checkpoint binding; the implementation must preserve the exact number or reject it actionably before snapshot mutation." + }, + { + "id": "foundation.security.ctx_metadata_credentials", + "area": "security", + "stage": "nonblocking_hardening", + "expectation": "Documentation and the validator docstring truthfully describe ctx_metadata screening as best-effort defense in depth, distinguish adopter principal metadata from buyer wire context, direct credentials to the supported write-only facility, and preserve current redaction and documented actual positive/negative behavior without claiming unknown metadata is safe." + }, + { + "id": "foundation.security.resource_location_credential_screen", + "area": "security", + "stage": "nonblocking_hardening", + "evidence_scope": "in_process_installed_seller_persistence_guards_only", + "advisory_only": true, + "does_not_override_secret_leak_failure": true, + "expectation": "Run the same 17 synthetic sentinels against each installed seller's public resource-location persistence guard and retain each native result. Report the bare JWT, cloud-key identifier, and encoded-presigned misses separately from intentional clean-HTTPS/provider-native naming policy differences; do not infer buyer validation, an HTTP quadrant break, or seven equivalent defects. Any future TypeScript hardening needs an explicit compatibility and false-positive budget, bounded decoding, and preservation of legitimate provider-native locations rather than rejecting every URL parse failure." + }, + { + "id": "foundation.pagination.zero_rows", + "area": "pagination", + "stage": "foundation", + "expectation": "A zero-row report retains exact totals, digests, status, and terminal pagination semantics." + }, + { + "id": "foundation.pagination.more_than_500_rows", + "area": "pagination", + "stage": "foundation", + "expectation": "More than 500 rows traverse every public page exactly once with stable continuation, order, totals, digests, and no truncation." + }, + { + "id": "foundation.webhook.signed_retry_dedup", + "area": "server_foundation_notifications", + "stage": "foundation", + "server_scope": "python_reporting_foundation", + "expectation": "The public Python reporting foundation signs callbacks over canonical bytes and persists bounded retry and deduplication across restart without logging secrets." + }, + { + "id": "foundation.webhook.protocol_rc4_vectors", + "area": "server_foundation_notifications", + "stage": "foundation", + "protocol_owned_inputs": true, + "expectation": "Execute all 29 byte-identical protocol rc.4 webhook-signing vectors through each installed public receiver composition. Preserve normative error code and failed-step classification, express revocation-staleness through the required webhook_receiver_runner contract rather than a bare verifier, and never count the separate 13-vector HTTP/replay control as complete protocol conformance." + }, + { + "id": "foundation.webhook.cross_language_signer_verifier", + "area": "server_foundation_notifications", + "stage": "foundation", + "signature_profile": "webhook_signature_unpadded_base64url", + "protocol_source_commit": "94976657c8456e5ad6de55d9793a883542a4fc5f", + "expectation": "Using the same protocol test key, body, URL, and clock, every public webhook emission path uses the route-selected unpadded Base64URL Signature profile and both installed signer-to-verifier directions interoperate without weakening mixed-alphabet rejection or request-signing profiles. Retain Python-to-TypeScript, TypeScript-to-Python, and both same-language controls with each emitted Signature alphabet; preserve vector-compatible Content-Digest bytes as a separately bounded wording discrepancy." + }, + { + "id": "foundation.activity.durable_account_feed", + "area": "server_foundation_notifications", + "stage": "foundation", + "server_scope": "python_reporting_foundation", + "expectation": "Receipt and consumer-status effects create durable, ordered, account-isolated public account activity exactly once." + }, + { + "id": "foundation.runtime.late_account_materialization_fairness", + "area": "server_foundation_runtime", + "stage": "foundation", + "server_scope": "python_reporting_foundation", + "confirmed_red_mechanism": "While the first account remains continuously due, the retained pre-update served_at continuation chases that account's own newly advanced timestamp and can prevent wrap to a later due account at negative infinity.", + "expectation": "After one account completes, a later admitted account reusing configuration and destination keys makes bounded autonomous progress from a real revision through verified materialization and accepted receipt with a size-one PostgreSQL pool, both lexical orders, uninterrupted first-account wakeups, and process restart. The regression must prove progress under continuous first-account dueness; it must not weaken the finding into permanent starvation after a due gap." + }, + { + "id": "foundation.runtime.producer_finality_temporal_order", + "area": "server_foundation_runtime", + "stage": "foundation", + "server_scope": "python_reporting_foundation", + "requires_server_capability": "reporting_producer", + "confirmed_red_mechanism": "A provider can observe and finalize source evidence during acquisition after dispatch, while the historical producer reuses its pre-fetch worker clock as revision.created_at, yielding finalized_at later than created_at and a buyer-side FINALITY_EVIDENCE_INVALID result.", + "expectation": "A real public source observation that completes after dispatch must be published with temporally valid revision evidence, then pass typed MCP/PostgreSQL status, exact-read, reconciliation, and replay/restart checks. Future, excessive-skew, and regressing-clock inputs remain separately fail-closed before publication. Equality and historical-time controls must remain definitive.", + "evidence_boundary": "The historical installed diagnostic is in-process public memory-producer evidence rather than HTTP/PostgreSQL acceptance; the original b203 public wire showed the inversion but stopped before buyer reconciliation." + }, + { + "id": "foundation.official_precedence.unlinked_both_artifacts", + "area": "official_precedence", + "stage": "foundation", + "historical_vector": "rc38_unlinked_unique_official_with_snapshot_and_official_artifacts", + "normative_rc4_fixture_coverage": false, + "supplemental_shared_regression": true, + "protocol_owned": false, + "expectation": "Select the unique official revision and its verified materialization as definitive even when it is not linked to the retained snapshot; ordering does not affect the result.", + "required_outcome": "corrected_behavior", + "known_defect_exemption": false + }, + { + "id": "foundation.official_precedence.no_official_materialization_unlinked", + "area": "official_precedence", + "stage": "foundation", + "historical_vector": "rc38_unique_official_without_materialization_unlinked", + "normative_rc4_fixture_coverage": false, + "supplemental_shared_regression": true, + "protocol_owned": false, + "expectation": "Select the unique official revision as nondefinitive with no selected artifact, no snapshot inspection or receipt submission, and no graph-integrity failure.", + "required_outcome": "corrected_behavior", + "known_defect_exemption": false + }, + { + "id": "foundation.official_precedence.no_official_materialization_linked", + "area": "official_precedence", + "stage": "foundation", + "historical_vector": "rc38_unique_official_without_materialization_linked_to_snapshot", + "normative_rc4_fixture_coverage": false, + "supplemental_shared_regression": true, + "protocol_owned": false, + "expectation": "Select the linked unique official revision as nondefinitive with no selected artifact, no snapshot inspection or receipt submission, and no graph-integrity failure.", + "required_outcome": "corrected_behavior", + "known_defect_exemption": false + }, + { + "id": "foundation.skew.actionable_unsupported", + "area": "version_skew", + "stage": "foundation", + "expectation": "A scenario outside an exact prior package feature envelope fails with an actionable capability or schema error rather than silent loss, manufactured success, or partial mutation." + }, + { + "id": "foundation.cli.reporting_storyboards", + "area": "cli_storyboards", + "stage": "foundation", + "expectation": "Resolve the exact five reporting storyboards and each step/stateful-step count from the installed TypeScript pin at run time, retain the frozen inventory, and execute every resolved step; historical rc.38 counts cannot be asserted against a newer pin." + }, + { + "id": "foundation.packaging.reject_version_only_candidate", + "area": "packaging", + "stage": "foundation", + "expectation": "Candidate Python installs use a local wheel or sdist path whose SHA-256, accepted source commit/tree, dependency record, and CPython runtime are bound into evidence; resolving the occupied 8.0.0b15 version from an index must fail the candidate identity gate." + }, + { + "id": "foundation.transport.response_headers_metadata", + "area": "transport", + "stage": "foundation", + "expectation": "Preserve diagnostic response headers in the public TypeScript transport metadata. The independently observed responseHeaders omission remains a visible nonblocking candidate defect until an exact installed pin demonstrates the declared Undici floor and current dependency paths." + }, + { + "id": "deferred.1172.service_composition", + "area": "deferred", + "stage": "deferred_1172", + "expectation": "Adapter-first service composition, lifecycle, scheduler, and mounted production service remain open under issue 1172." + }, + { + "id": "deferred.1172.client_reporting_facade", + "area": "deferred", + "stage": "deferred_1172", + "expectation": "A new Python client.reporting facade remains open; standalone public reporting functions are not represented as that facade." + }, + { + "id": "deferred.1182.provisional_reread", + "area": "deferred", + "stage": "deferred_1182", + "expectation": "Default provisional reread, cadence, immutable observation revisions, and provisional_until remain open under issue 1182." + } + ] +} diff --git a/scripts/ci/reporting_interop/npm/rc41/package-lock.json b/scripts/ci/reporting_interop/npm/rc41/package-lock.json new file mode 100644 index 000000000..0031e02a8 --- /dev/null +++ b/scripts/ci/reporting_interop/npm/rc41/package-lock.json @@ -0,0 +1,1729 @@ +{ + "name": "adcp-reporting-interop-rc41", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "adcp-reporting-interop-rc41", + "version": "0.0.0", + "dependencies": { + "@adcp/sdk": "14.0.0-rc.41" + } + }, + "node_modules/@a2a-js/sdk": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-1.2.0.tgz", + "integrity": "sha512-zxjvBrxhPV4J/RN8QTbsYaUIu9Y9RvmR06IP5o42UdJX42A9I0m1StGVvqrcCQ3bdu3SzWHPm5CeC2FNHARvkw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "jose": "^6.2.3" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@bufbuild/protobuf": "^2.10.2", + "@grpc/grpc-js": "^1.11.0", + "express": "^4.21.2 || ^5.1.0" + }, + "peerDependenciesMeta": { + "@bufbuild/protobuf": { + "optional": true + }, + "@grpc/grpc-js": { + "optional": true + }, + "express": { + "optional": true + } + } + }, + "node_modules/@adcp/sdk": { + "version": "14.0.0-rc.41", + "resolved": "https://registry.npmjs.org/@adcp/sdk/-/sdk-14.0.0-rc.41.tgz", + "integrity": "sha512-Qfs+ujKBpIjf7m9jcuzxvN/XCXUssxhR5xCvxCq4oqMR3aMJjnuTgzjxltPtv+A6LOxwmX1ia8sMRdhVSehdBg==", + "license": "Apache-2.0", + "workspaces": [ + ".", + "packages/*" + ], + "dependencies": { + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", + "@types/ws": "^8.18.1", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "fast-check": "^4.9.0", + "jose": "^6.2.10", + "pg": "^8.23.0", + "secure-json-parse": "^4.1.0", + "semver": "^7.8.5", + "structured-headers": "2.0.3", + "tldts": "^7.0.0", + "undici": "^6.28.0", + "ws": "^8.21.3", + "yaml": "^2.9.0" + }, + "bin": { + "adcp": "bin/adcp.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@a2a-js/sdk": "^1.0.1", + "@modelcontextprotocol/sdk": "^1.24.0", + "@opentelemetry/api": "^1.0.0", + "redis": "^4.6.0 || ^5.0.0 || ^6.0.0", + "zod": "^4.1.5" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "redis": { + "optional": true + } + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.17", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz", + "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz", + "integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "jose": "^6.1.3", + "pkce-challenge": "^5.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz", + "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==", + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/node": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0.tgz", + "integrity": "sha512-Y4hAC2XdGDUdDOCbLDOCA4+aL3NUldjsOWlDL/YwpAxrPhRm1xHd7lZ+mLacvZ9t3PaH28wgNoaLQGrIk1P2pg==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "^2.0.0", + "hono": "^4.11.4" + }, + "peerDependenciesMeta": { + "hono": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz", + "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@types/node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT", + "peer": true + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-check": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.10.2.tgz", + "integrity": "sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "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-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.8.tgz", + "integrity": "sha512-/Gng7NfoykZl2pjukW5Z6+8Yxm3BPRf86GTbQnt0SbySkvax4fyL4H3HhY1cCpBGmiW9XDRFzRV+CXK2W8QudQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "peer": true + }, + "node_modules/ip-address": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT", + "peer": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.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.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "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/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "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/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pure-rand": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz", + "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/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/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, + "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/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC", + "peer": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/structured-headers": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-2.0.3.tgz", + "integrity": "sha512-4g5yxhlDMClRwCcfKfLeS7Z8yAVdOWGDADwm80Poh1iReU2KVKLGBlqwpHWJ2qovq0+ZIf1atAEO1eua2o9Rgg==", + "license": "MIT", + "engines": { + "node": ">=18", + "npm": ">=6" + } + }, + "node_modules/tldts": { + "version": "7.4.14", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.14.tgz", + "integrity": "sha512-EahQoi+Q5oqmG3yxRHx+bwn36OaLbtw6jnTFcGjc8fcmktzTgk45xRnzAx8Ch87PFEiqRhoc3FWBmY+LBrvEGQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.14" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.14", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.14.tgz", + "integrity": "sha512-KYkjfJHnIC5t+Gy7hPrMHgJmWc/N9FfmS+fggRPSlpFckidpB9HD6OzPsSTkfDH+MpZgcu2tJPGgLfHl979N1Q==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/undici": { + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "peer": true + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "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/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peer": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/scripts/ci/reporting_interop/npm/rc41/package.json b/scripts/ci/reporting_interop/npm/rc41/package.json new file mode 100644 index 000000000..b79252fba --- /dev/null +++ b/scripts/ci/reporting_interop/npm/rc41/package.json @@ -0,0 +1,8 @@ +{ + "name": "adcp-reporting-interop-rc41", + "version": "0.0.0", + "private": true, + "dependencies": { + "@adcp/sdk": "14.0.0-rc.41" + } +} diff --git a/scripts/ci/reporting_interop/npm/rc42/package-lock.json b/scripts/ci/reporting_interop/npm/rc42/package-lock.json new file mode 100644 index 000000000..834e6e2b3 --- /dev/null +++ b/scripts/ci/reporting_interop/npm/rc42/package-lock.json @@ -0,0 +1,1730 @@ +{ + "name": "ts-rc42-inspect", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ts-rc42-inspect", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@adcp/sdk": "https://registry.npmjs.org/@adcp/sdk/-/sdk-14.0.0-rc.42.tgz" + } + }, + "node_modules/@a2a-js/sdk": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-1.2.0.tgz", + "integrity": "sha512-zxjvBrxhPV4J/RN8QTbsYaUIu9Y9RvmR06IP5o42UdJX42A9I0m1StGVvqrcCQ3bdu3SzWHPm5CeC2FNHARvkw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "jose": "^6.2.3" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@bufbuild/protobuf": "^2.10.2", + "@grpc/grpc-js": "^1.11.0", + "express": "^4.21.2 || ^5.1.0" + }, + "peerDependenciesMeta": { + "@bufbuild/protobuf": { + "optional": true + }, + "@grpc/grpc-js": { + "optional": true + }, + "express": { + "optional": true + } + } + }, + "node_modules/@adcp/sdk": { + "version": "14.0.0-rc.42", + "resolved": "https://registry.npmjs.org/@adcp/sdk/-/sdk-14.0.0-rc.42.tgz", + "integrity": "sha512-5v0byus3w2SKfXgZkT+ljuXoys/yjRLxSv2SXCuuRhXts4qyE0mYhO65GFR4WQWTVanAP6uBtIpZ5CLzfxii2Q==", + "license": "Apache-2.0", + "workspaces": [ + ".", + "packages/*" + ], + "dependencies": { + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", + "@types/ws": "^8.18.1", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "fast-check": "^4.9.0", + "jose": "^6.2.10", + "pg": "^8.23.0", + "secure-json-parse": "^4.1.0", + "semver": "^7.8.5", + "structured-headers": "2.0.3", + "tldts": "^7.0.0", + "undici": "^6.28.0", + "ws": "^8.21.3", + "yaml": "^2.9.0" + }, + "bin": { + "adcp": "bin/adcp.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@a2a-js/sdk": "^1.0.1", + "@modelcontextprotocol/sdk": "^1.24.0", + "@opentelemetry/api": "^1.0.0", + "redis": "^4.6.0 || ^5.0.0 || ^6.0.0", + "zod": "^4.1.5" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "redis": { + "optional": true + } + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.17", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz", + "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz", + "integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "jose": "^6.1.3", + "pkce-challenge": "^5.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz", + "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==", + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/node": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0.tgz", + "integrity": "sha512-Y4hAC2XdGDUdDOCbLDOCA4+aL3NUldjsOWlDL/YwpAxrPhRm1xHd7lZ+mLacvZ9t3PaH28wgNoaLQGrIk1P2pg==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "^2.0.0", + "hono": "^4.11.4" + }, + "peerDependenciesMeta": { + "hono": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz", + "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@types/node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT", + "peer": true + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-check": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.10.2.tgz", + "integrity": "sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "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-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.8.tgz", + "integrity": "sha512-/Gng7NfoykZl2pjukW5Z6+8Yxm3BPRf86GTbQnt0SbySkvax4fyL4H3HhY1cCpBGmiW9XDRFzRV+CXK2W8QudQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "peer": true + }, + "node_modules/ip-address": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT", + "peer": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.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.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "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/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "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/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pure-rand": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz", + "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/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/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, + "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/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC", + "peer": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/structured-headers": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-2.0.3.tgz", + "integrity": "sha512-4g5yxhlDMClRwCcfKfLeS7Z8yAVdOWGDADwm80Poh1iReU2KVKLGBlqwpHWJ2qovq0+ZIf1atAEO1eua2o9Rgg==", + "license": "MIT", + "engines": { + "node": ">=18", + "npm": ">=6" + } + }, + "node_modules/tldts": { + "version": "7.4.14", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.14.tgz", + "integrity": "sha512-EahQoi+Q5oqmG3yxRHx+bwn36OaLbtw6jnTFcGjc8fcmktzTgk45xRnzAx8Ch87PFEiqRhoc3FWBmY+LBrvEGQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.14" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.14", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.14.tgz", + "integrity": "sha512-KYkjfJHnIC5t+Gy7hPrMHgJmWc/N9FfmS+fggRPSlpFckidpB9HD6OzPsSTkfDH+MpZgcu2tJPGgLfHl979N1Q==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/undici": { + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "peer": true + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "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/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peer": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/scripts/ci/reporting_interop/npm/rc42/package.json b/scripts/ci/reporting_interop/npm/rc42/package.json new file mode 100644 index 000000000..132828c93 --- /dev/null +++ b/scripts/ci/reporting_interop/npm/rc42/package.json @@ -0,0 +1,15 @@ +{ + "name": "ts-rc42-inspect", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@adcp/sdk": "https://registry.npmjs.org/@adcp/sdk/-/sdk-14.0.0-rc.42.tgz" + } +} diff --git a/scripts/ci/reporting_interop/npm/rc44/package-lock.json b/scripts/ci/reporting_interop/npm/rc44/package-lock.json new file mode 100644 index 000000000..2baf9f745 --- /dev/null +++ b/scripts/ci/reporting_interop/npm/rc44/package-lock.json @@ -0,0 +1,1771 @@ +{ + "name": "ts-rc44-inspect", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ts-rc44-inspect", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@adcp/sdk": "14.0.0-rc.44" + } + }, + "node_modules/@a2a-js/sdk": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-1.2.0.tgz", + "integrity": "sha512-zxjvBrxhPV4J/RN8QTbsYaUIu9Y9RvmR06IP5o42UdJX42A9I0m1StGVvqrcCQ3bdu3SzWHPm5CeC2FNHARvkw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "jose": "^6.2.3" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@bufbuild/protobuf": "^2.10.2", + "@grpc/grpc-js": "^1.11.0", + "express": "^4.21.2 || ^5.1.0" + }, + "peerDependenciesMeta": { + "@bufbuild/protobuf": { + "optional": true + }, + "@grpc/grpc-js": { + "optional": true + }, + "express": { + "optional": true + } + } + }, + "node_modules/@adcp/sdk": { + "version": "14.0.0-rc.44", + "resolved": "https://registry.npmjs.org/@adcp/sdk/-/sdk-14.0.0-rc.44.tgz", + "integrity": "sha512-PxivQs0Gqd/nV3TWiVR+zFn9ZF6b1R8ZVRqK9u5Oep/Dyo4NFxR4q/eFFa4vXRrsfKb9fLohDz8wRc53jn+UoQ==", + "license": "Apache-2.0", + "workspaces": [ + ".", + "packages/*" + ], + "dependencies": { + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/ext-apps": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", + "@types/ws": "^8.18.1", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "fast-check": "^4.9.0", + "jose": "^6.2.10", + "pg": "^8.23.0", + "secure-json-parse": "^4.1.0", + "semver": "^7.8.5", + "structured-headers": "2.0.3", + "tldts": "^7.0.0", + "undici": "^6.28.0", + "ws": "^8.21.3", + "yaml": "^2.9.0" + }, + "bin": { + "adcp": "bin/adcp.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@a2a-js/sdk": "^1.0.1", + "@modelcontextprotocol/sdk": "^1.24.0", + "@opentelemetry/api": "^1.0.0", + "redis": "^4.6.0 || ^5.0.0 || ^6.0.0", + "zod": "^4.1.5" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "redis": { + "optional": true + } + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.17", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz", + "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz", + "integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "jose": "^6.1.3", + "pkce-challenge": "^5.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz", + "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==", + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/ext-apps": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-2.0.0.tgz", + "integrity": "sha512-a6tXzFcIbIIdnqumQ7W8Oxd8W/KAPkAKYpoxpD9nDgxZ24ywgxG5pK/8G9W5pOFUygS7gWHQhPv8cwRB44/8yg==", + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "dependencies": { + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/client": "^2.0.0", + "@modelcontextprotocol/core": "^2.0.0", + "@modelcontextprotocol/server": "^2.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "zod": "^4.2.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/server": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/node": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0.tgz", + "integrity": "sha512-Y4hAC2XdGDUdDOCbLDOCA4+aL3NUldjsOWlDL/YwpAxrPhRm1xHd7lZ+mLacvZ9t3PaH28wgNoaLQGrIk1P2pg==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "^2.0.0", + "hono": "^4.11.4" + }, + "peerDependenciesMeta": { + "hono": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz", + "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT", + "peer": true + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-check": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.10.2.tgz", + "integrity": "sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "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-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.8.tgz", + "integrity": "sha512-/Gng7NfoykZl2pjukW5Z6+8Yxm3BPRf86GTbQnt0SbySkvax4fyL4H3HhY1cCpBGmiW9XDRFzRV+CXK2W8QudQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "peer": true + }, + "node_modules/ip-address": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT", + "peer": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.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.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "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/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "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/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pure-rand": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz", + "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/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/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, + "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/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC", + "peer": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/structured-headers": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-2.0.3.tgz", + "integrity": "sha512-4g5yxhlDMClRwCcfKfLeS7Z8yAVdOWGDADwm80Poh1iReU2KVKLGBlqwpHWJ2qovq0+ZIf1atAEO1eua2o9Rgg==", + "license": "MIT", + "engines": { + "node": ">=18", + "npm": ">=6" + } + }, + "node_modules/tldts": { + "version": "7.4.14", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.14.tgz", + "integrity": "sha512-EahQoi+Q5oqmG3yxRHx+bwn36OaLbtw6jnTFcGjc8fcmktzTgk45xRnzAx8Ch87PFEiqRhoc3FWBmY+LBrvEGQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.14" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.14", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.14.tgz", + "integrity": "sha512-KYkjfJHnIC5t+Gy7hPrMHgJmWc/N9FfmS+fggRPSlpFckidpB9HD6OzPsSTkfDH+MpZgcu2tJPGgLfHl979N1Q==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/undici": { + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "peer": true + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "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/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peer": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/scripts/ci/reporting_interop/npm/rc44/package.json b/scripts/ci/reporting_interop/npm/rc44/package.json new file mode 100644 index 000000000..2d4f4c836 --- /dev/null +++ b/scripts/ci/reporting_interop/npm/rc44/package.json @@ -0,0 +1,15 @@ +{ + "name": "ts-rc44-inspect", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@adcp/sdk": "14.0.0-rc.44" + } +} diff --git a/scripts/ci/reporting_interop/npm/rc45/package-lock.json b/scripts/ci/reporting_interop/npm/rc45/package-lock.json new file mode 100644 index 000000000..df486c83d --- /dev/null +++ b/scripts/ci/reporting_interop/npm/rc45/package-lock.json @@ -0,0 +1,1770 @@ +{ + "name": "reporting-interop-ts-rc45", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "reporting-interop-ts-rc45", + "version": "1.0.0", + "dependencies": { + "@adcp/sdk": "14.0.0-rc.45" + } + }, + "node_modules/@a2a-js/sdk": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-1.2.0.tgz", + "integrity": "sha512-zxjvBrxhPV4J/RN8QTbsYaUIu9Y9RvmR06IP5o42UdJX42A9I0m1StGVvqrcCQ3bdu3SzWHPm5CeC2FNHARvkw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "jose": "^6.2.3" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@bufbuild/protobuf": "^2.10.2", + "@grpc/grpc-js": "^1.11.0", + "express": "^4.21.2 || ^5.1.0" + }, + "peerDependenciesMeta": { + "@bufbuild/protobuf": { + "optional": true + }, + "@grpc/grpc-js": { + "optional": true + }, + "express": { + "optional": true + } + } + }, + "node_modules/@adcp/sdk": { + "version": "14.0.0-rc.45", + "resolved": "https://registry.npmjs.org/@adcp/sdk/-/sdk-14.0.0-rc.45.tgz", + "integrity": "sha512-ywglF0pBXDUfxW6IUdXMf+xKAkIKFezxKf+Jq7gDuY2NdqCg6Ctgi496pUYk23oFiBEe8NRjzWadjWzSo/+J7w==", + "license": "Apache-2.0", + "workspaces": [ + ".", + "packages/*" + ], + "dependencies": { + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/ext-apps": "2.0.0", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", + "@types/ws": "^8.18.1", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "fast-check": "^4.9.0", + "jose": "^6.2.10", + "pg": "^8.23.0", + "secure-json-parse": "^4.1.0", + "semver": "^7.8.5", + "structured-headers": "2.0.3", + "tldts": "^7.0.0", + "undici": "^6.28.0", + "ws": "^8.21.3", + "yaml": "^2.9.0" + }, + "bin": { + "adcp": "bin/adcp.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@a2a-js/sdk": "^1.0.1", + "@modelcontextprotocol/sdk": "^1.24.0", + "@opentelemetry/api": "^1.0.0", + "redis": "^4.6.0 || ^5.0.0 || ^6.0.0", + "zod": "^4.1.5" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "redis": { + "optional": true + } + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.17", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz", + "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz", + "integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "jose": "^6.1.3", + "pkce-challenge": "^5.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz", + "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==", + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/ext-apps": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-2.0.0.tgz", + "integrity": "sha512-a6tXzFcIbIIdnqumQ7W8Oxd8W/KAPkAKYpoxpD9nDgxZ24ywgxG5pK/8G9W5pOFUygS7gWHQhPv8cwRB44/8yg==", + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "dependencies": { + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/client": "^2.0.0", + "@modelcontextprotocol/core": "^2.0.0", + "@modelcontextprotocol/server": "^2.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "zod": "^4.2.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/server": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/node": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0.tgz", + "integrity": "sha512-Y4hAC2XdGDUdDOCbLDOCA4+aL3NUldjsOWlDL/YwpAxrPhRm1xHd7lZ+mLacvZ9t3PaH28wgNoaLQGrIk1P2pg==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "^2.0.0", + "hono": "^4.11.4" + }, + "peerDependenciesMeta": { + "hono": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz", + "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT", + "peer": true + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-check": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.10.2.tgz", + "integrity": "sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "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-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.8.tgz", + "integrity": "sha512-/Gng7NfoykZl2pjukW5Z6+8Yxm3BPRf86GTbQnt0SbySkvax4fyL4H3HhY1cCpBGmiW9XDRFzRV+CXK2W8QudQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "peer": true + }, + "node_modules/ip-address": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT", + "peer": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.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.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "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/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "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/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pure-rand": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz", + "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/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/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, + "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/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC", + "peer": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/structured-headers": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-2.0.3.tgz", + "integrity": "sha512-4g5yxhlDMClRwCcfKfLeS7Z8yAVdOWGDADwm80Poh1iReU2KVKLGBlqwpHWJ2qovq0+ZIf1atAEO1eua2o9Rgg==", + "license": "MIT", + "engines": { + "node": ">=18", + "npm": ">=6" + } + }, + "node_modules/tldts": { + "version": "7.4.14", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.14.tgz", + "integrity": "sha512-EahQoi+Q5oqmG3yxRHx+bwn36OaLbtw6jnTFcGjc8fcmktzTgk45xRnzAx8Ch87PFEiqRhoc3FWBmY+LBrvEGQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.14" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.14", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.14.tgz", + "integrity": "sha512-KYkjfJHnIC5t+Gy7hPrMHgJmWc/N9FfmS+fggRPSlpFckidpB9HD6OzPsSTkfDH+MpZgcu2tJPGgLfHl979N1Q==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/undici": { + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "peer": true + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "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/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peer": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/scripts/ci/reporting_interop/npm/rc45/package.json b/scripts/ci/reporting_interop/npm/rc45/package.json new file mode 100644 index 000000000..ac762dba2 --- /dev/null +++ b/scripts/ci/reporting_interop/npm/rc45/package.json @@ -0,0 +1,8 @@ +{ + "name": "reporting-interop-ts-rc45", + "version": "1.0.0", + "private": true, + "dependencies": { + "@adcp/sdk": "14.0.0-rc.45" + } +} diff --git a/scripts/ci/reporting_interop/official_precedence/cases.json b/scripts/ci/reporting_interop/official_precedence/cases.json new file mode 100644 index 000000000..1e5fa5b59 --- /dev/null +++ b/scripts/ci/reporting_interop/official_precedence/cases.json @@ -0,0 +1,24 @@ +{ + "schema_version": 1, + "protocol_owned": false, + "scope": "supplemental_shared_neutral_regression", + "cases": [ + {"id":"official_precedence/snapshot/both_artifacts/unlinked/natural","required":"returned; reportingRevisionId=revision-august-official; definitive=true; reportingMaterializationId=materialization-billing; reasons=[]"}, + {"id":"official_precedence/snapshot/both_artifacts/unlinked/reversed","required":"returned; reportingRevisionId=revision-august-official; definitive=true; reportingMaterializationId=materialization-billing; reasons=[]"}, + {"id":"official_precedence/snapshot/both_artifacts/linked/natural","required":"returned; reportingRevisionId=revision-august-official; definitive=true; reportingMaterializationId=materialization-billing; reasons=[]"}, + {"id":"official_precedence/snapshot/both_artifacts/linked/reversed","required":"returned; reportingRevisionId=revision-august-official; definitive=true; reportingMaterializationId=materialization-billing; reasons=[]"}, + {"id":"official_precedence/snapshot/official_without_materialization/unlinked/natural","required":"returned; reportingRevisionId=revision-august-official; definitive=false; reportingMaterializationId=undefined"}, + {"id":"official_precedence/snapshot/official_without_materialization/unlinked/reversed","required":"returned; reportingRevisionId=revision-august-official; definitive=false; reportingMaterializationId=undefined"}, + {"id":"official_precedence/snapshot/official_without_materialization/linked/natural","required":"returned; reportingRevisionId=revision-august-official; definitive=false; reportingMaterializationId=undefined"}, + {"id":"official_precedence/snapshot/official_without_materialization/linked/reversed","required":"returned; reportingRevisionId=revision-august-official; definitive=false; reportingMaterializationId=undefined"}, + {"id":"official_precedence/official/both_artifacts/unlinked/natural","required":"returned; reportingRevisionId=revision-august-official; definitive=true; reportingMaterializationId=materialization-billing; reasons=[]"}, + {"id":"official_precedence/official/both_artifacts/unlinked/reversed","required":"returned; reportingRevisionId=revision-august-official; definitive=true; reportingMaterializationId=materialization-billing; reasons=[]"}, + {"id":"official_precedence/official/both_artifacts/linked/natural","required":"returned; reportingRevisionId=revision-august-official; definitive=true; reportingMaterializationId=materialization-billing; reasons=[]"}, + {"id":"official_precedence/official/both_artifacts/linked/reversed","required":"returned; reportingRevisionId=revision-august-official; definitive=true; reportingMaterializationId=materialization-billing; reasons=[]"}, + {"id":"official_precedence/official/official_without_materialization/unlinked/natural","required":"returned; reportingRevisionId=revision-august-official; definitive=false; reportingMaterializationId=undefined"}, + {"id":"official_precedence/official/official_without_materialization/unlinked/reversed","required":"returned; reportingRevisionId=revision-august-official; definitive=false; reportingMaterializationId=undefined"}, + {"id":"official_precedence/official/official_without_materialization/linked/natural","required":"returned; reportingRevisionId=revision-august-official; definitive=false; reportingMaterializationId=undefined"}, + {"id":"official_precedence/official/official_without_materialization/linked/reversed","required":"returned; reportingRevisionId=revision-august-official; definitive=false; reportingMaterializationId=undefined"}, + {"id":"official_precedence/single_official","required":"returned; reportingRevisionId=revision-august-official; definitive=true; reportingMaterializationId=materialization-billing; reasons=[]"} + ] +} diff --git a/scripts/ci/reporting_interop/official_precedence/fixture.json b/scripts/ci/reporting_interop/official_precedence/fixture.json new file mode 100644 index 000000000..ccedc3e23 --- /dev/null +++ b/scripts/ci/reporting_interop/official_precedence/fixture.json @@ -0,0 +1,302 @@ +{ + "wire": { + "status": "completed", + "view": "periods", + "ledger_snapshot_id": "snapshot-before-receipt", + "ledger_as_of": "2026-09-02T00:00:06Z", + "account_id": "account-1", + "scope": { + "period_start": "2026-08-01T00:00:00Z", + "period_end": "2026-09-01T00:00:00Z", + "scope_closed": true, + "media_buy_ids": [ + "buy-1", + "buy-2" + ], + "all_accessible_media_buys": true, + "delivery_config_generations": [ + { + "delivery_config_id": "billing-feed", + "delivery_config_version": 1, + "feed_purpose": "billing" + } + ], + "feed_purposes": [ + "billing" + ], + "finality": [ + "official" + ], + "ledger_retained_from": "2026-07-01T00:00:00Z", + "coverage_complete": true + }, + "periods": [ + { + "reporting_obligation_id": "obligation-billing", + "delivery_config_id": "billing-feed", + "delivery_config_version": 1, + "report_definition_id": "billing-v1", + "feed_purpose": "billing", + "reporting_profile": "billing-v1", + "account_id": "account-1", + "media_buy_ids": [ + "buy-1", + "buy-2" + ], + "scope_resolved_at": "2026-09-01T00:00:00Z", + "coverage": { + "status": "full", + "evaluated_at": "2026-09-01T00:00:00Z", + "media_buy_ids": [ + "buy-1", + "buy-2" + ], + "fully_covered_media_buy_ids": [ + "buy-1", + "buy-2" + ], + "partially_covered_media_buy_ids": [], + "unsupported_media_buy_ids": [], + "unknown_media_buy_ids": [], + "package_ids": [ + "package-1", + "package-2" + ], + "covered_package_ids": [ + "package-1", + "package-2" + ], + "unsupported_package_ids": [], + "unknown_package_ids": [], + "limitations": [] + }, + "period": { + "start": "2026-08-01T00:00:00Z", + "end": "2026-09-01T00:00:00Z", + "source_timezone": "UTC" + }, + "expected_at": "2026-09-02T00:00:00Z", + "schedule": { + "period_duration": "P1M", + "alignment": "billing_cycle", + "period_anchor": "2026-01-01T00:00:00Z", + "period_timezone": "UTC", + "delivery_sla": "P1D" + }, + "destination_ref": "destination-obligation-billing", + "required_finality": "official", + "reconciliation_mode": "consumer_receipt", + "reconciliation_status": "pending", + "health": "waiting", + "production_status": "published", + "revision_count": 1, + "materialization_count": 1, + "successful_materialization_count": 1, + "receipt_count": 0, + "accepted_receipt_count": 0, + "issues": [], + "resource_retained_until": "2026-12-01T00:00:00Z" + } + ], + "revisions": [ + { + "reporting_revision_id": "revision-august-official", + "revision_content_sha256": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "report_definition_id": "billing-v1", + "report_definition_uri": "https://schemas.example/report-definitions/billing-v1.json", + "report_definition_sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "reporting_profile": "billing-v1", + "schema_version": "1", + "schema_uri": "https://schemas.example/billing-v1.json", + "schema_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "account_id": "account-1", + "media_buy_ids": [ + "buy-1", + "buy-2" + ], + "coverage": { + "status": "full", + "evaluated_at": "2026-09-01T00:00:00Z", + "media_buy_ids": [ + "buy-1", + "buy-2" + ], + "fully_covered_media_buy_ids": [ + "buy-1", + "buy-2" + ], + "partially_covered_media_buy_ids": [], + "unsupported_media_buy_ids": [], + "unknown_media_buy_ids": [], + "package_ids": [ + "package-1", + "package-2" + ], + "covered_package_ids": [ + "package-1", + "package-2" + ], + "unsupported_package_ids": [], + "unknown_package_ids": [], + "limitations": [] + }, + "period": { + "start": "2026-08-01T00:00:00Z", + "end": "2026-09-01T00:00:00Z", + "source_timezone": "UTC" + }, + "finality": "official", + "finality_basis": "contractual_cutoff", + "finality_policy_id": "billing-close-v1", + "finalized_at": "2026-09-02T00:00:00Z", + "observed_at": "2026-09-02T00:00:00Z", + "data_through": "2026-09-01T00:00:00Z", + "data_through_precision": "exact", + "row_count": 7, + "control_totals": [ + { + "name": "impressions", + "value": "4200", + "value_type": "integer", + "unit": "impressions" + }, + { + "name": "spend", + "value": "7000.00", + "value_type": "decimal", + "unit": "USD" + } + ], + "canonical_content_digest": { + "algorithm": "sha256", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "canonicalization_id": "rows-v1", + "canonicalization_uri": "https://schemas.example/canonicalization/rows-v1.json", + "canonicalization_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "created_at": "2026-09-02T00:00:00Z" + } + ], + "materializations": [ + { + "reporting_materialization_id": "materialization-billing", + "reporting_revision_id": "revision-august-official", + "reporting_obligation_id": "obligation-billing", + "delivery_config_id": "billing-feed", + "delivery_config_version": 1, + "destination_ref": "destination-obligation-billing", + "feed_purpose": "billing", + "method": "dataset_share", + "transport": "delta_sharing", + "attempt": 1, + "status": "available", + "ready_at": "2026-09-02T00:00:05Z", + "resource": { + "resource_ref": "resource-materialization-billing", + "kind": "dataset", + "location": "share.billing", + "native_version_ref": "version-42", + "immutability": "native_version", + "expires_at": "2026-12-01T00:00:00Z" + }, + "verification": { + "verified_at": "2026-09-02T00:00:05Z", + "verification_path": "representative_consumer", + "verification_profile": "canonical_digest", + "row_count": 7, + "control_totals": [ + { + "name": "impressions", + "value": "4200", + "value_type": "integer", + "unit": "impressions" + }, + { + "name": "spend", + "value": "7000.00", + "value_type": "decimal", + "unit": "USD" + } + ], + "canonical_content_digest": { + "algorithm": "sha256", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "canonicalization_id": "rows-v1", + "canonicalization_uri": "https://schemas.example/canonicalization/rows-v1.json", + "canonicalization_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } + }, + "created_at": "2026-09-02T00:00:01Z" + } + ], + "receipts": [], + "pagination": { + "has_more": false, + "total_count": 3 + } + }, + "expected": { + "deliveryConfigId": "billing-feed", + "deliveryConfigVersion": 1, + "reportDefinitionId": "billing-v1", + "feedPurpose": "billing", + "reportingProfile": "billing-v1", + "mediaBuyIds": [ + "buy-1", + "buy-2" + ], + "destinationRef": "destination-obligation-billing", + "deliveryMethod": "dataset_share", + "requiredFinality": "official", + "reconciliationMode": "consumer_receipt", + "coverageRequirement": "full", + "coverage": { + "status": "full", + "media_buy_ids": [ + "buy-1", + "buy-2" + ], + "fully_covered_media_buy_ids": [ + "buy-1", + "buy-2" + ], + "partially_covered_media_buy_ids": [], + "unsupported_media_buy_ids": [], + "unknown_media_buy_ids": [], + "package_ids": [ + "package-1", + "package-2" + ], + "covered_package_ids": [ + "package-1", + "package-2" + ], + "unsupported_package_ids": [], + "unknown_package_ids": [] + }, + "reportDefinitionUri": "https://schemas.example/report-definitions/billing-v1.json", + "reportDefinitionSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "schemaVersion": "1", + "schemaUri": "https://schemas.example/billing-v1.json", + "schemaSha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "schemaDialect": "https://json-schema.org/draft/2020-12/schema", + "schemaRefPolicy": "local_fragment_only", + "officialFinality": { + "policyId": "billing-close-v1", + "basis": "contractual_cutoff" + }, + "verificationProfile": "canonical_digest", + "canonicalization": { + "id": "rows-v1", + "uri": "https://schemas.example/canonicalization/rows-v1.json", + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "primaryKeys": [ + "media_buy_id" + ] + }, + "periodStart": "2026-08-01T00:00:00Z", + "periodEnd": "2026-09-01T00:00:00Z" + } +} diff --git a/scripts/ci/reporting_interop/official_precedence/pins-rc45.json b/scripts/ci/reporting_interop/official_precedence/pins-rc45.json new file mode 100644 index 000000000..018d1371f --- /dev/null +++ b/scripts/ci/reporting_interop/official_precedence/pins-rc45.json @@ -0,0 +1,17 @@ +{ + "package": "@adcp/sdk", + "version": "14.0.0-rc.45", + "integrity": "sha512-ywglF0pBXDUfxW6IUdXMf+xKAkIKFezxKf+Jq7gDuY2NdqCg6Ctgi496pUYk23oFiBEe8NRjzWadjWzSo/+J7w==", + "shasum": "5f11aa19ee02908b8c42eefdd08ef5d1eec74374", + "tarballSha256": "a0952ed8edaaad958bdb8f474c4f5cb68333ee272e7a8f3419d12930e9c57e6b", + "gitHead": "94171fe20d632f027eb362604c715ed361fadb51", + "sourceTree": "8d3c29d9a1779f04520c33cdc9ed3e15e5296cdd", + "defaultAdcpVersion": "3.2.0-rc.4", + "fixtureSha256": "dc16c44297a315122c4450bdb4c7278b5635894f7a60fb0266456b8d083da013", + "historicalPin": { + "version": "14.0.0-rc.38", + "gitHead": "97fc49687069b29e56c8b2e155b055c3a03dda0c", + "defaultAdcpVersion": "3.2.0-rc.3", + "note": "Historical lead only. Its compiled-hash and version assertions are deliberately not inherited." + } +} diff --git a/scripts/ci/reporting_interop/official_precedence/pins.json b/scripts/ci/reporting_interop/official_precedence/pins.json new file mode 100644 index 000000000..0b543ae81 --- /dev/null +++ b/scripts/ci/reporting_interop/official_precedence/pins.json @@ -0,0 +1,17 @@ +{ + "package": "@adcp/sdk", + "version": "14.0.0-rc.42", + "integrity": "sha512-5v0byus3w2SKfXgZkT+ljuXoys/yjRLxSv2SXCuuRhXts4qyE0mYhO65GFR4WQWTVanAP6uBtIpZ5CLzfxii2Q==", + "shasum": "ec78a3912293fe450236ff9b31b81640ba4b493b", + "tarballSha256": "63fd1930026c5bf0774199536f9b6082b45fd056a5b6d803f018a77d5356a748", + "gitHead": "7b963da36eeca5ce7872af0bc429b1ee5abb0cb6", + "defaultAdcpVersion": "3.2.0-rc.4", + "fixtureSha256": "dc16c44297a315122c4450bdb4c7278b5635894f7a60fb0266456b8d083da013", + "observedCompiledReconcilerSha256": "1f2362aec97463d5397a940a15a82bc69a9210d863297f37fcb9ff5fc792327a", + "historicalPin": { + "version": "14.0.0-rc.38", + "gitHead": "97fc49687069b29e56c8b2e155b055c3a03dda0c", + "defaultAdcpVersion": "3.2.0-rc.3", + "note": "Historical lead only. Its compiled-hash and version assertions are deliberately not inherited." + } +} diff --git a/scripts/ci/reporting_interop/official_precedence/repro-candidate.cjs b/scripts/ci/reporting_interop/official_precedence/repro-candidate.cjs new file mode 100644 index 000000000..36050c318 --- /dev/null +++ b/scripts/ci/reporting_interop/official_precedence/repro-candidate.cjs @@ -0,0 +1,12 @@ +'use strict'; + +// Preserve the rc.42-adapted semantic runner unchanged while selecting the +// separately pinned candidate identity for this process. Node caches JSON +// module objects, so mutating the already-loaded historical pin object makes +// the existing runner consume pins-rc45.json without altering its source or +// the byte-exact fixture. +const historicalPins = require('./pins.json'); +const candidatePins = require('./pins-rc45.json'); +for (const key of Object.keys(historicalPins)) delete historicalPins[key]; +Object.assign(historicalPins, candidatePins); +require('./repro-rc42.cjs'); diff --git a/scripts/ci/reporting_interop/official_precedence/repro-rc42.cjs b/scripts/ci/reporting_interop/official_precedence/repro-rc42.cjs new file mode 100644 index 000000000..44d48cc49 --- /dev/null +++ b/scripts/ci/reporting_interop/official_precedence/repro-rc42.cjs @@ -0,0 +1,305 @@ +'use strict'; +// rc.42-adapted derivative of the historical RC.38 buyer reproduction. +// Fixture bytes are byte-identical to the historical input (hash-asserted). +// Differences from the historical probe, all deliberate: +// * pins/version/schema identity re-resolved at rc.42 (ADCP_VERSION 3.2.0-rc.4). +// * the historical compiled-reconciler hash assertion is NOT inherited; rc.42 +// bytes are recorded instead. +// * NO expected-defect exemption. Every one of the 17 inputs must meet the +// correct semantics. capture() classifies any error rather than aborting. +// Fixture data adapted from adcontextprotocol/adcp-client, Apache-2.0, +// copyright 2025 AgenticAdvertising.Org. +const assert = require('node:assert/strict'); +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); +const sdk = require('@adcp/sdk'); +const { TOOL_RESPONSE_SCHEMAS, getCanonicalToolValidator } = require('@adcp/sdk/schemas'); +const pins = require('./pins.json'); +const fixtureBytes = fs.readFileSync(path.join(__dirname, 'fixture.json')); +const fixture = JSON.parse(fixtureBytes); +const sha256 = v => crypto.createHash('sha256').update(v).digest('hex'); +const officialId = 'revision-august-official'; +const snapshotId = 'revision-august-snapshot'; +const officialMaterializationId = 'materialization-billing'; +const now = new Date('2026-09-03T00:00:00Z'); + +// ---- identity gate (independent, at the actual pin) ---- +assert.equal(sha256(fixtureBytes), pins.fixtureSha256, 'fixture bytes changed'); +const packageFile = require.resolve('@adcp/sdk/package.json'); +const installed = require(packageFile); +assert.equal(installed.version, pins.version, 'unexpected SDK version'); +assert.equal(sdk.ADCP_VERSION, pins.defaultAdcpVersion, 'unexpected default AdCP version'); +const sdkRoot = path.dirname(packageFile); +const compiled = path.join(sdkRoot, 'dist/lib/reporting/reconciliation.js'); +const compiledSha = sha256(fs.readFileSync(compiled)); +const publicEntry = fs.realpathSync(require.resolve('@adcp/sdk')); +assert.ok(publicEntry.startsWith(fs.realpathSync(sdkRoot) + path.sep), 'entry escaped package root'); +for (const n of ['evaluateReportingLedger', 'reconcileReporting', 'buildReportingReceipt']) + assert.equal(typeof sdk[n], 'function', 'missing public API: ' + n); +const canonicalValidator = getCanonicalToolValidator('get_reporting_status', 'sync', { + adcpVersion: pins.defaultAdcpVersion, +}); +assert.equal(typeof canonicalValidator, 'function', 'missing public canonical response validator'); + +// ---- case construction: identical to the historical makeCase ---- +function makeCase(parameters) { + const { requiredFinality, officialArtifact, linked, reversed, singleOfficial = false } = parameters; + const wire = structuredClone(fixture.wire); + const expected = { ...structuredClone(fixture.expected), requiredFinality }; + Object.assign(wire.periods[0], { required_finality: requiredFinality, health: 'complete' }); + wire.scope.finality = ['snapshot', 'official']; + wire.ledger_as_of = '2026-09-02T00:02:00Z'; + wire.ledger_snapshot_id = 'snapshot-complete-repro-history'; + const official = structuredClone(wire.revisions[0]); + const snapshot = { + ...structuredClone(official), + reporting_revision_id: snapshotId, + finality: 'snapshot', + observed_at: '2026-09-01T00:00:00Z', + created_at: '2026-09-01T00:00:00Z', + }; + for (const key of ['finality_basis', 'finality_policy_id', 'finalized_at']) delete snapshot[key]; + if (linked) official.supersedes_reporting_revision_id = snapshotId; + const officialMaterialization = structuredClone(wire.materializations[0]); + const snapshotMaterialization = { + ...structuredClone(officialMaterialization), + reporting_materialization_id: 'materialization-snapshot', + reporting_revision_id: snapshotId, + }; + wire.revisions = singleOfficial ? [official] : [snapshot, official]; + wire.materializations = singleOfficial ? [officialMaterialization] : [ + snapshotMaterialization, ...(officialArtifact ? [officialMaterialization] : []), + ]; + wire.receipts = wire.materializations.map((materialization, index) => sdk.buildReportingReceipt( + { + obligation: wire.periods[0], + revision: wire.revisions.find(i => i.reporting_revision_id === materialization.reporting_revision_id), + materialization, + expected, + }, + { + rowCount: 7, + controlTotals: structuredClone(fixture.wire.revisions[0].control_totals), + canonicalContentDigest: structuredClone(fixture.wire.revisions[0].canonical_content_digest), + }, + 'reporting-receipt-probe-' + index, + '2026-09-02T00:01:00Z', + )); + Object.assign(wire.periods[0], { + revision_count: wire.revisions.length, + materialization_count: wire.materializations.length, + successful_materialization_count: wire.materializations.length, + receipt_count: wire.receipts.length, + accepted_receipt_count: wire.receipts.length, + pending_adjustment_count: 0, + reconciliation_status: officialArtifact ? 'accepted' : 'pending', + health: officialArtifact ? 'complete' : 'action_required', + issues: officialArtifact ? [] : [{ + issue_id: 'issue-official-receipt-required', + code: 'RECEIPT_REQUIRED', + severity: 'action_required', + responsible_party: 'seller', + recommended_action: 'contact_seller', + reporting_obligation_id: 'obligation-billing', + }], + }); + if (reversed) for (const key of ['revisions', 'materializations', 'receipts']) wire[key].reverse(); + wire.pagination.total_count = wire.periods.length + wire.revisions.length + + wire.materializations.length + wire.receipts.length; + return { wire, expected }; +} + +// ---- independent input validation at the actual pin ---- +function validateInput(wire, expected, parameters) { + const v = { errors: [] }; + const parsed = TOOL_RESPONSE_SCHEMAS.get_reporting_status.safeParse(wire); + v.convenienceSchemaValid = parsed.success; + if (!parsed.success) v.errors.push({ validator: 'zod_convenience', issues: parsed.error.issues.slice(0, 6) }); + v.canonicalSchemaValid = Boolean(canonicalValidator(wire)); + if (!v.canonicalSchemaValid) v.errors.push({ validator: 'canonical_ajv', issues: (canonicalValidator.errors || []).slice(0, 6) }); + // structural/ownership invariants (same checks as the historical probe) + const s = []; + const ob = wire.periods[0]; + const ids = new Map(wire.revisions.map(i => [i.reporting_revision_id, i])); + const expectedCount = parameters.singleOfficial ? 1 : parameters.officialArtifact ? 2 : 1; + const chk = (cond, label) => { if (!cond) s.push(label); }; + chk(wire.status === 'completed', 'status'); + chk(wire.view === 'periods', 'view'); + chk(wire.account_id === 'account-1', 'account_id'); + chk(wire.periods.length === 1, 'one_obligation'); + chk(wire.scope.scope_closed === true, 'scope_closed'); + chk(wire.scope.coverage_complete === true, 'coverage_complete'); + chk(wire.pagination.has_more === false, 'pagination_terminated'); + chk(wire.pagination.cursor === undefined, 'no_cursor'); + chk(JSON.stringify(wire.scope.finality) === JSON.stringify(['snapshot', 'official']), 'scope_finality'); + chk(ob.required_finality === parameters.requiredFinality, 'required_finality'); + chk(ob.reconciliation_mode === 'consumer_receipt', 'reconciliation_mode'); + chk(ob.pending_adjustment_count === 0, 'pending_adjustment_count'); + chk(JSON.stringify(ob.issues.map(i => i.code)) === JSON.stringify(parameters.officialArtifact ? [] : ['RECEIPT_REQUIRED']), 'issue_codes'); + chk(ids.size === wire.revisions.length, 'unique_revision_ids'); + const official = ids.get(officialId); + chk(official && official.finality === 'official', 'official_finality'); + chk(official && official.supersedes_reporting_revision_id === (parameters.linked ? snapshotId : undefined), 'supersession_link'); + chk(wire.materializations.length === expectedCount, 'materialization_count'); + chk(wire.receipts.length === expectedCount, 'receipt_count'); + chk(wire.materializations.some(i => i.reporting_revision_id === officialId) === parameters.officialArtifact, 'official_artifact_presence'); + for (const r of wire.revisions) { + for (const k of ['account_id', 'report_definition_id', 'reporting_profile', 'media_buy_ids', 'coverage', 'period']) + chk(JSON.stringify(r[k]) === JSON.stringify(ob[k]), 'revision_ownership:' + k); + chk(Date.parse(r.created_at) <= Date.parse(wire.ledger_as_of), 'revision_time_bound'); + } + for (const m of wire.materializations) { + chk(ids.has(m.reporting_revision_id), 'materialization_joined'); + chk(m.status === 'available', 'materialization_available'); + const rec = wire.receipts.filter(i => i.reporting_materialization_id === m.reporting_materialization_id); + chk(rec.length === 1 && rec[0].status === 'accepted', 'one_accepted_receipt'); + if (rec[0]) chk(Date.parse(rec[0].observed_at) <= Date.parse(wire.ledger_as_of), 'receipt_time_bound'); + } + chk(wire.pagination.total_count === 1 + wire.revisions.length + expectedCount * 2, 'total_count'); + v.structuralViolations = s; + v.inputValid = v.convenienceSchemaValid && v.canonicalSchemaValid && s.length === 0; + return v; +} + +function toLedger(wire) { + return { + ledgerSnapshotId: wire.ledger_snapshot_id, ledgerAsOf: wire.ledger_as_of, + accountId: wire.account_id, scope: wire.scope, obligations: wire.periods, + revisions: wire.revisions, materializations: wire.materializations, receipts: wire.receipts, + }; +} + +// classify every outcome; never abort on an unexpected error +async function capture(action) { + try { + const result = await action(); + const obligations = result && result.obligations; + if (!Array.isArray(obligations) || obligations.length !== 1) + return { kind: 'unexpected_shape', obligationCount: Array.isArray(obligations) ? obligations.length : null }; + return { kind: 'returned', obligation: obligations[0] }; + } catch (error) { + return { kind: 'threw', code: error.code, name: error.name, message: error.message }; + } +} + +// required-correct semantics (README "Required result" column); no defect exemption +function semanticPass(outcome, officialArtifact) { + if (outcome.kind !== 'returned') return false; + const item = outcome.obligation; + if (item.reportingRevisionId !== officialId) return false; + if (!officialArtifact) return item.definitive === false && item.reportingMaterializationId === undefined; + return item.definitive === true && item.reportingMaterializationId === officialMaterializationId && + Array.isArray(item.reasons) && item.reasons.length === 0; +} +function requiredDescription(officialArtifact) { + return officialArtifact + ? `returned; reportingRevisionId=${officialId}; definitive=true; reportingMaterializationId=${officialMaterializationId}; reasons=[]` + : `returned; reportingRevisionId=${officialId}; definitive=false; reportingMaterializationId=undefined`; +} +function summarize(o) { + if (o.kind !== 'returned') return o; + const i = o.obligation; + return { kind: 'returned', reportingRevisionId: i.reportingRevisionId, + reportingMaterializationId: i.reportingMaterializationId, definitive: i.definitive, + reasons: i.reasons, obligationKeys: Object.keys(i).sort() }; +} + +async function main() { + const cases = []; + for (const requiredFinality of ['snapshot', 'official']) + for (const officialArtifact of [true, false]) + for (const linked of [false, true]) + for (const reversed of [false, true]) cases.push({ requiredFinality, officialArtifact, linked, reversed }); + cases.push({ requiredFinality: 'official', officialArtifact: true, linked: false, reversed: false, singleOfficial: true }); + + const rows = []; + for (const parameters of cases) { + const { wire, expected } = makeCase(parameters); + const validation = validateInput(wire, expected, parameters); + const before = structuredClone(wire); + const effects = []; + let statusReads = 0; + const direct = await capture(() => sdk.evaluateReportingLedger(toLedger(wire), [expected], now)); + const buyer = await capture(() => sdk.reconcileReporting({ + client: { + async getReportingStatus() { statusReads += 1; return structuredClone(wire); }, + async syncReportingReceipts() { effects.push('receipt'); throw new Error('unexpected receipt submission'); }, + }, + request: { account: { account_id: 'account-1' } }, + expectedPeriods: [expected], + async inspect(ctx) { effects.push('inspect:' + ctx.revision.reporting_revision_id); throw new Error('unexpected inspection'); }, + now, + })); + const mutated = JSON.stringify(wire) !== JSON.stringify(before); + const positiveControl = Boolean(parameters.singleOfficial || (parameters.linked && parameters.officialArtifact)); + const pass = semanticPass(direct, parameters.officialArtifact); + rows.push({ + caseId: `official_precedence/${parameters.singleOfficial ? 'single_official' : + `${parameters.requiredFinality}/${parameters.officialArtifact ? 'both_artifacts' : 'official_without_materialization'}/${parameters.linked ? 'linked' : 'unlinked'}/${parameters.reversed ? 'reversed' : 'natural'}`}`, + parameters, positiveControl, + inputSha256: sha256(JSON.stringify({ wire, expected })), + validation: { convenienceSchemaValid: validation.convenienceSchemaValid, + canonicalSchemaValid: validation.canonicalSchemaValid, + structuralViolations: validation.structuralViolations, inputValid: validation.inputValid, + errors: validation.errors }, + required: requiredDescription(parameters.officialArtifact), + evaluateReportingLedger: summarize(direct), + reconcileReporting: summarize(buyer), + apisAgree: JSON.stringify(summarize(direct)) === JSON.stringify(summarize(buyer)), + callerHistoryMutated: mutated, statusReads, sideEffects: effects, + semanticPass: pass, + observedDefect: pass ? null + : direct.kind === 'threw' ? `graph_error:${direct.code || direct.name}` + : direct.kind === 'returned' && Array.isArray(direct.obligation.reasons) && direct.obligation.reasons.length + ? `nondefinitive:${direct.obligation.reasons.join(',')}` + : `other:${direct.kind}`, + }); + } + const report = { + scope: 'synthetic_in_process_public_buyer_apis_only__NOT_http_quadrant_acceptance', + derivedFrom: { historicalBundle: 'rc38 official-precedence reproduction', + fixtureSha256: pins.fixtureSha256, historicalPin: pins.historicalPin, + defectExemptionsRemoved: true }, + pin: { package: pins.package, version: installed.version, integrity: pins.integrity, + shasum: pins.shasum, tarballSha256: pins.tarballSha256, gitHead: pins.gitHead, + adcpVersion: sdk.ADCP_VERSION, engines: installed.engines }, + runtime: { node: process.version, platform: process.platform, arch: process.arch }, + source: { fixtureSha256: sha256(fixtureBytes), compiledReconcilerSha256: compiledSha, + compiledReconcilerBytes: fs.statSync(compiled).size }, + totals: { + inputs: rows.length, + convenienceSchemaValid: rows.filter(r => r.validation.convenienceSchemaValid).length, + canonicalSchemaValid: rows.filter(r => r.validation.canonicalSchemaValid).length, + structurallyValid: rows.filter(r => r.validation.structuralViolations.length === 0).length, + publicApiCalls: rows.length * 2, + apisAgree: rows.filter(r => r.apisAgree).length, + callerHistoryMutations: rows.filter(r => r.callerHistoryMutated).length, + unexpectedSideEffects: rows.reduce((n, r) => n + r.sideEffects.length, 0), + semanticPass: rows.filter(r => r.semanticPass).length, + semanticFail: rows.filter(r => !r.semanticPass).length, + positiveControls: rows.filter(r => r.positiveControl).length, + positiveControlsPassing: rows.filter(r => r.positiveControl && r.semanticPass).length, + ambiguityDefectCases: rows.filter(r => !r.positiveControl && r.parameters.officialArtifact).length, + ambiguityDefectReproduced: rows.filter(r => !r.positiveControl && r.parameters.officialArtifact && + r.observedDefect === 'nondefinitive:AMBIGUOUS_REVISION_CHAIN').length, + missingArtifactCases: rows.filter(r => !r.parameters.officialArtifact).length, + missingArtifactGraphErrors: rows.filter(r => !r.parameters.officialArtifact && + r.observedDefect === 'graph_error:LEDGER_GRAPH_INTEGRITY_FAILED').length, + }, + semanticCompatibilityPassed: rows.every(r => r.semanticPass), + allInputsValid: rows.every(r => r.validation.inputValid), + cases: rows, + }; + const outFile = process.argv[2]; + if (outFile) fs.writeFileSync(outFile, JSON.stringify(report, null, 2) + '\n'); + else process.stdout.write(JSON.stringify(report, null, 2) + '\n'); + console.error(JSON.stringify(report.totals, null, 2)); + console.error('allInputsValid = ' + report.allInputsValid); + console.error('semanticCompatibilityPassed = ' + report.semanticCompatibilityPassed); + process.exitCode = report.semanticCompatibilityPassed && report.allInputsValid ? 0 : 1; +} +main().catch(e => { + process.stderr.write(JSON.stringify({ kind: 'harness_failure', message: e.message, stack: String(e.stack).split('\n').slice(0,4) }) + '\n'); + process.exitCode = 2; +}); diff --git a/scripts/ci/reporting_interop/pins.json b/scripts/ci/reporting_interop/pins.json new file mode 100644 index 000000000..a8a7cadcb --- /dev/null +++ b/scripts/ci/reporting_interop/pins.json @@ -0,0 +1,237 @@ +{ + "schema_version": 1, + "pending_source_leads": { + "python_main": { + "commit": "940c95e0c2d93758ed334b3edff59cbe933362d4", + "status": "accepted_source_base_not_selected_artifact" + }, + "python_pr1208": { + "head": "a124afd3", + "status": "focused_source_green_no_final_artifact_pin" + }, + "typescript_calendar": { + "pr3018_head": "not_present_in_supplied_evidence", + "pr3018_squash_merge": "f3c7accb", + "published_version": "14.0.0-rc.47", + "registry_git_head": "b0d2886f0f5b8668fc134568c4cd22a4ef89e2fa", + "source_tree": "12515822c5814dcf2a0bb3c4d92e7b3ad023df46", + "source_lineage": { + "pull_request": 3018, + "reviewed_pr_head": null, + "reviewed_pr_head_status": "not_recorded_in_current_harness_evidence", + "squash_merge_commit_prefix": "f3c7accb", + "published_registry_git_head": "b0d2886f0f5b8668fc134568c4cd22a4ef89e2fa", + "identities_are_not_interchangeable": true + }, + "tarball_sha256": "1b14aeddab973809f1850e189f833ef645d061e25c82d74046c97fd92d346e49", + "integrity": "sha512-Zi2r9CZ2HJmB7i8XTlt5Z1QrVq+nnKkTcl8fSQUkjsi3ChhVtR24BIu/6Gg7yz5ctBWfMR2Lxqn4wv8kh3Wygw==", + "protocol": "3.2.0-rc.6", + "status": "development_selected_registry_qualified_not_locked_installed_or_executed" + } + }, + "protocol": { + "required_contract": { + "version": "3.2.0-rc.6", + "bundle_selected": false, + "status": "required_alignment_not_a_selected_protocol_bundle" + }, + "historical_fixture_rc4": { + "version": "3.2.0-rc.4", + "release_commit": "94976657c8456e5ad6de55d9793a883542a4fc5f", + "bundle_sha256": "773bee016d279345d6fae91a0ce684a22ca9b81c2edd2cdb9a71dbfea65954d2" + }, + "previous": { + "version": "3.2.0-rc.3", + "release_commit": "71f9cd5414454e94ccfef87ce25777ead6fad228", + "bundle_sha256": "1cd940f76516b43327cbfa6c17c0befd4c093ca85b53a81a4062d38bb522d831" + } + }, + "python": { + "candidate": { + "package": "adcp", + "version": "8.0.0b15", + "source_commit": "25e0c7278a19493345975881d1578c76fc64dc1b", + "source_tree": "bcc4ad30ea4ee43d7b82f39e12f60b5a3ac2aead", + "source_parents": [ + "7af569985174235739be4007e693d10ccb604817", + "fe1a1cbd070bc94ec32685026ad39ec55058dc73" + ], + "adoption_base": "1f953c40d761be71d11fde84c78359ff2074fe7c", + "build_kind": "prepublication_exact_pr1208_head", + "artifact_policy": "local_path_plus_sha256_plus_source_tree_plus_dependency_runtime_record", + "version_string_occupied": true, + "known_independent_builds": [ + { + "builder": "sdk_owner_reference", + "vcs_wheel_sha256": "0e7e189b98a1e512c6273547c341a458dcebcc15691268178bddf56e42385a28", + "sdist_wheel_sha256": "b6a7a68f188b021e2bc11c8fa6b4e86aff7eaf1311c7eeb2d6ad90c7d60133fc", + "wheel_bytes": 89865398, + "sdist_sha256": "c77b77a19a2a2eb5fc5804b7dcf500ae06cf87994ec4d8d441275bc7e5c06c91", + "sdist_bytes": 84213583 + }, + { + "builder": "python_expert", + "installed_wheel_sha256": "5beb75ed7ef9a406184c3049efd67e87d08d05c46fa1a4b28eab09a7271bc268", + "wheel_bytes": 89865399, + "qualification": "separate_six_case_public_composition_evidence_not_matrix_coverage" + }, + { + "builder": "reporting_interop_harness", + "vcs_wheel_sha256": "11987e93f01dfab250071238fd78d94fa646a2de2c468a38396a7103ba19c327", + "sdist_wheel_sha256": "a5afc1a02a91ec6a0f06bc301c0d92d2a1013173786274f0620bcfdcd5175e73", + "wheel_bytes": 89865398, + "sdist_sha256": "b52f7bd8bf25361ccbca98705a7201744cd30e252c171fa91aa829731fce83b1", + "sdist_bytes": 84200303, + "wheel_member_count": 7813, + "sdk_member_count": 7807, + "all_member_manifest_sha256": "397b9b58ad4e72d68e1380a5ad86ff9a76852ca873c3466b78d7c9890035cfa5", + "sdk_member_manifest_sha256": "0ed3b8de391a04be9aee477c6db11aa82e0edca12b1651ab9935dc83ba73b333", + "metadata_sha256": "cfae23f8b2a21b0425a7122dfea285d7a1edf0afdb9428b995df3919d95ca28a", + "build": { + "python": "3.10.21", + "build": "1.6.1", + "setuptools": "84.0.0", + "wheel": "0.48.0" + } + } + ], + "development_dependency_controls": [ + { + "python": "3.10.21", + "pydantic": "2.13.0", + "mcp": "2.0.0", + "lock": "scripts/ci/reporting_interop/python/requirements-py310-pydantic2130-mcp200.txt", + "lock_sha256": "83d81a1b400c33aa6536bc2002f11656b7a7b4dc88db37eda5faa7da7a85599f" + }, + { + "python": "3.10.21", + "pydantic": "2.13.5", + "mcp": "2.2.0", + "lock": "scripts/ci/reporting_interop/python/requirements-py310-pydantic2135-mcp220.txt", + "lock_sha256": "1e6d45d094fd24b61e749ed0d658ed1d9f6a5e54e7977bc8cbefe3fcabeccb69" + } + ], + "protocol": "3.2.0-rc.4", + "python_floor": "3.10" + }, + "previous_released": { + "package": "adcp", + "version": "8.0.0b15", + "tag": "v8.0.0-beta.15", + "source_commit": "3e76aa54623529a3dda01cd690b8a5c287c75641", + "wheel_url": "https://files.pythonhosted.org/packages/8b/63/cf3d7b2885126771009f6da3786ba564fcae7e910568c93f06cf672d2f5e/adcp-8.0.0b15-py3-none-any.whl", + "wheel_sha256": "608636a4fe774bc4846b5bb0eab6367d0b8e69f6f371aa6085845d9f8329f48f", + "wheel_bytes": 40403241, + "sdist_sha256": "a6f20df5b17f011686de83aa4c787b234a6c1b6ddca2967633253b53294ae86b", + "sdist_bytes": 71947572, + "protocol": "3.2.0-rc.3", + "feature_envelope": "buyer_core_and_typed_reporting_client_only" + }, + "latest_stable_canary": { + "observed_version": "7.0.2", + "protocol": "3.1.15", + "blocking": false, + "feature_envelope": "reporting_unsupported" + } + }, + "typescript": { + "development_candidate_rc47": { + "package": "@adcp/sdk", + "version": "14.0.0-rc.47", + "integrity": "sha512-Zi2r9CZ2HJmB7i8XTlt5Z1QrVq+nnKkTcl8fSQUkjsi3ChhVtR24BIu/6Gg7yz5ctBWfMR2Lxqn4wv8kh3Wygw==", + "tarball_sha256": "1b14aeddab973809f1850e189f833ef645d061e25c82d74046c97fd92d346e49", + "registry_git_head": "b0d2886f0f5b8668fc134568c4cd22a4ef89e2fa", + "source_tree": "12515822c5814dcf2a0bb3c4d92e7b3ad023df46", + "protocol": "3.2.0-rc.6", + "package_engines_node": "^20.19.0 || >=22.12.0", + "development_runtime_pin": "22.12.0", + "development_runtime_status": "selected_not_executed_by_this_harness", + "selection_scope": "development_aligned_Q1_Q4_only", + "executable_harness_input": false, + "package_lock": null, + "transitive_lock": null, + "installed_member_binding": null, + "retained_member_evidence": { + "universal_compliance_subset_members": 55, + "scope": "universal_compliance_subset_only_not_complete_package_binding", + "complete_installed_members_expected": 6261, + "complete_installed_member_binding_available": false, + "can_substitute_for_transitive_lock": false + }, + "qualification": { + "independent_source_review": "approved", + "calendar_suite": 8, + "calendar_case_c1": 12, + "calendar_case_c2": 8, + "t1_response_headers": "fixed", + "node_timezone_identities": "pinned_in_expert_evidence_not_yet_copied_to_harness" + }, + "acceptance": false + }, + "historical_candidate_rc45": { + "package": "@adcp/sdk", + "version": "14.0.0-rc.45", + "tarball": "https://registry.npmjs.org/@adcp/sdk/-/sdk-14.0.0-rc.45.tgz", + "integrity": "sha512-ywglF0pBXDUfxW6IUdXMf+xKAkIKFezxKf+Jq7gDuY2NdqCg6Ctgi496pUYk23oFiBEe8NRjzWadjWzSo/+J7w==", + "shasum": "5f11aa19ee02908b8c42eefdd08ef5d1eec74374", + "tarball_sha256": "a0952ed8edaaad958bdb8f474c4f5cb68333ee272e7a8f3419d12930e9c57e6b", + "tarball_bytes": 19598459, + "tarball_members": 6246, + "registry_git_head": "94171fe20d632f027eb362604c715ed361fadb51", + "source_tree": "8d3c29d9a1779f04520c33cdc9ed3e15e5296cdd", + "package_lock": "scripts/ci/reporting_interop/npm/rc45/package-lock.json", + "package_lock_sha256": "fc344df04de5bbc3182e70cb184ea2df0e5cbc8bd164299ea3f551a33ab91c57", + "protocol": "3.2.0-rc.4", + "node_floor": "22.12.0", + "blocking": false, + "qualification": "historical_rc4_development_input_not_required_rc6_candidate" + }, + "historical_candidate_rc42": { + "package": "@adcp/sdk", + "version": "14.0.0-rc.42", + "tarball": "https://registry.npmjs.org/@adcp/sdk/-/sdk-14.0.0-rc.42.tgz", + "integrity": "sha512-5v0byus3w2SKfXgZkT+ljuXoys/yjRLxSv2SXCuuRhXts4qyE0mYhO65GFR4WQWTVanAP6uBtIpZ5CLzfxii2Q==", + "shasum": "ec78a3912293fe450236ff9b31b81640ba4b493b", + "tarball_sha256": "63fd1930026c5bf0774199536f9b6082b45fd056a5b6d803f018a77d5356a748", + "registry_git_head": "7b963da36eeca5ce7872af0bc429b1ee5abb0cb6", + "package_lock": "scripts/ci/reporting_interop/npm/rc42/package-lock.json", + "package_lock_sha256": "a5d0819de6aea8ce36605550f4f0c95232c6552c376837a4bf7fe3c782b75516", + "protocol": "3.2.0-rc.4", + "qualification": "historical_matrix12_red_not_current_candidate" + }, + "previous_compatible": { + "package": "@adcp/sdk", + "version": "14.0.0-rc.41", + "tarball": "https://registry.npmjs.org/@adcp/sdk/-/sdk-14.0.0-rc.41.tgz", + "integrity": "sha512-Qfs+ujKBpIjf7m9jcuzxvN/XCXUssxhR5xCvxCq4oqMR3aMJjnuTgzjxltPtv+A6LOxwmX1ia8sMRdhVSehdBg==", + "shasum": "394b62123fb31c02aeec92f7f12874142fac9a09", + "tarball_sha256": "c9bb1e22b63dfedf6bc1ae9c07d1a1312465489671e9841dd5f1425b817b0a90", + "registry_git_head": "51bf16d1b5425bd9aa2f37e7960ec9c1634d5fc6", + "package_lock": "scripts/ci/reporting_interop/npm/rc41/package-lock.json", + "package_lock_sha256": "2059bd6794ff8f2f06f5ec83187ba7de5607f262b9b0a18a4d0d0be5682efa74", + "protocol": "3.2.0-rc.4", + "node_floor": "22.12.0" + }, + "latest_canary": { + "observed_version": "13.1.1", + "observed_at": "2026-09-25T00:00:00Z", + "observation_provenance": "independent_exact_registry_readback_reported_to_coordinator", + "execution_policy": "resolve_and_record_exact_current_identity_at_execution", + "blocking": false, + "feature_envelope": "resolve_and_classify_at_execution" + }, + "newer_rc_lead": { + "version": "14.0.0-rc.44", + "integrity": "sha512-PxivQs0Gqd/nV3TWiVR+zFn9ZF6b1R8ZVRqK9u5Oep/Dyo4NFxR4q/eFFa4vXRrsfKb9fLohDz8wRc53jn+UoQ==", + "shasum": "ffddf5664cd654d2c668290f4d76d096d770eed2", + "tarball_sha256": "f74c30ba4e113984b26ee95ea9a77e1138848a913ed43e8c94c0ec256ea62fb5", + "registry_git_head": "9043b770e32e7916b107894f1a1d226a1f388d48", + "package_lock": "scripts/ci/reporting_interop/npm/rc44/package-lock.json", + "package_lock_sha256": "868d8384bea8628dfc38ff7d7bfa6cb4ea9ead85efc1c002744b8cd305214e38", + "protocol": "3.2.0-rc.4", + "blocking": false, + "qualification": "historical_supplemental_core_lead_not_current_candidate", + "public_core_buyer": "reconcileReportingCoreV1" + } + } +} diff --git a/scripts/ci/reporting_interop/python/requirements-py310-pydantic2130-mcp200.txt b/scripts/ci/reporting_interop/python/requirements-py310-pydantic2130-mcp200.txt new file mode 100644 index 000000000..f537f7f1f --- /dev/null +++ b/scripts/ci/reporting_interop/python/requirements-py310-pydantic2130-mcp200.txt @@ -0,0 +1,57 @@ +# Install the hash-verified local adcp artifact separately; this locks dependencies only. +a2a-sdk==1.0.1 +aiologic==0.17.1 +annotated-types==0.8.0 +anyio==4.15.1 +attrs==26.1.0 +certifi==2026.7.22 +cffi==2.1.1 +charset-normalizer==3.5.1 +click==8.5.0 +cryptography==50.0.1 +culsans==0.11.0 +dnspython==2.8.0 +email-validator==2.3.0 +exceptiongroup==1.3.1 +filelock==4.0.1 +google-api-core==2.38.0 +google-auth==2.58.0 +googleapis-common-protos==1.75.3 +h11==0.16.0 +httpcore==1.0.9 +httpcore2==2.13.0 +httpx==0.28.1 +httpx-sse==0.4.3 +httpx2==2.13.0 +idna==3.20 +json-rpc==1.15.0 +jsonschema==4.26.0 +jsonschema-specifications==2025.9.1 +mcp==2.0.0 +mcp-types==2.0.0 +opentelemetry-api==1.44.0 +packaging==26.3 +proto-plus==1.28.4 +protobuf==7.36.2 +pyasn1==0.6.4 +pyasn1-modules==0.4.2 +pycparser==3.0 +pydantic==2.13.0 +pydantic-core==2.46.0 +pyjwt==2.14.0 +python-multipart==0.0.32 +referencing==0.37.0 +requests==2.34.2 +requests-file==3.0.1 +rfc8785==0.1.4 +rpds-py==0.30.0 +sniffio==1.3.1 +sse-starlette==3.4.11 +starlette==1.6.0 +tldextract==5.3.2 +truststore==0.10.4 +typing-extensions==4.16.0 +typing-inspection==0.4.4 +urllib3==2.8.0 +uvicorn==0.53.0 +wrapt==2.4.1 diff --git a/scripts/ci/reporting_interop/python/requirements-py310-pydantic2135-mcp220.txt b/scripts/ci/reporting_interop/python/requirements-py310-pydantic2135-mcp220.txt new file mode 100644 index 000000000..a5409ec3c --- /dev/null +++ b/scripts/ci/reporting_interop/python/requirements-py310-pydantic2135-mcp220.txt @@ -0,0 +1,60 @@ +# Install the hash-verified local adcp artifact separately; this locks dependencies only. +a2a-sdk==1.0.1 +aiologic==0.17.1 +annotated-types==0.8.0 +anyio==4.15.1 +attrs==26.1.0 +certifi==2026.7.22 +cffi==2.1.1 +charset-normalizer==3.5.1 +click==8.5.0 +cryptography==50.0.1 +culsans==0.11.0 +dnspython==2.8.0 +email-validator==2.3.0 +exceptiongroup==1.3.1 +filelock==4.0.1 +google-api-core==2.38.0 +google-auth==2.58.0 +googleapis-common-protos==1.75.3 +h11==0.16.0 +httpcore==1.0.9 +httpcore2==2.13.0 +httpx==0.28.1 +httpx-sse==0.4.3 +httpx2==2.13.0 +idna==3.20 +json-rpc==1.15.0 +jsonschema==4.26.0 +jsonschema-specifications==2025.9.1 +mcp==2.2.0 +mcp-types==2.2.0 +opentelemetry-api==1.44.0 +packaging==26.3 +proto-plus==1.28.4 +protobuf==7.36.2 +psycopg==3.3.6 +psycopg-binary==3.3.6 +psycopg-pool==3.3.2 +pyasn1==0.6.4 +pyasn1-modules==0.4.2 +pycparser==3.0 +pydantic==2.13.5 +pydantic-core==2.46.5 +pyjwt==2.14.0 +python-multipart==0.0.32 +referencing==0.37.0 +requests==2.34.2 +requests-file==3.0.1 +rfc8785==0.1.4 +rpds-py==0.30.0 +sniffio==1.3.1 +sse-starlette==3.4.11 +starlette==1.6.0 +tldextract==5.3.2 +truststore==0.10.4 +typing-extensions==4.16.0 +typing-inspection==0.4.4 +urllib3==2.8.0 +uvicorn==0.53.0 +wrapt==2.4.1 diff --git a/scripts/ci/reporting_interop/python_core_client.py b/scripts/ci/reporting_interop/python_core_client.py new file mode 100644 index 000000000..a6121c0e8 --- /dev/null +++ b/scripts/ci/reporting_interop/python_core_client.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Installed Python typed-client and semantic Core reconciliation probe.""" + +from __future__ import annotations + +import argparse +import asyncio +import json +import os +from datetime import datetime, timezone +from importlib.metadata import version + +from adcp import ADCPClient, AgentConfig +from adcp.exceptions import ConfigurationError +from adcp.reporting import ExpectedReportingPeriod, reconcile_reporting_core +from adcp.types import GetReportingStatusRequest, Protocol + + +def _arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--url", required=True) + parser.add_argument("--auth-env", required=True) + parser.add_argument("--account", required=True) + parser.add_argument("--adcp-version", required=True) + parser.add_argument("--expect-unsupported", action="store_true") + return parser.parse_args() + + +async def run(args: argparse.Namespace) -> dict[str, object]: + token = os.environ.get(args.auth_env) + if not token: + raise RuntimeError(f"authentication environment variable is unset: {args.auth_env}") + client = ADCPClient( + AgentConfig( + id="interop-seller", + name="interop-seller", + agent_uri=args.url, + protocol=Protocol.MCP, + auth_token=token, + auth_header="Authorization", + auth_type="bearer", + ), + adcp_version=args.adcp_version, + ) + request = GetReportingStatusRequest.model_validate( + { + "account": {"account_id": args.account}, + "view": "periods", + "period": { + "start": "2026-08-01T00:00:00Z", + "end": "2026-08-02T00:00:00Z", + }, + } + ) + async with client: + typed = await client.get_reporting_status(request) + result = await reconcile_reporting_core( + client, + request, + expected_periods=[ + ExpectedReportingPeriod( + "shared-daily-key", + 1, + "interop-core-v1", + "analytics", + "interop-core-v1", + (f"media-buy-{args.account[-1]}",), + "2026-08-01T00:00:00Z", + "2026-08-02T00:00:00Z", + ) + ], + now=datetime(2026, 9, 3, tzinfo=timezone.utc), + ) + return { + "kind": "python_core_semantic_probe", + "package": {"name": "adcp", "version": version("adcp")}, + "protocol": args.adcp_version, + "typed_status": typed.status.value, + "definitive": result.definitive, + "obligations": [ + { + "definitive": item.definitive, + "reasons": list(item.reasons), + "reporting_revision_id": item.reporting_revision_id, + } + for item in result.obligations + ], + "submitted_receipts": len(result.submitted_receipts), + } + + +def main() -> None: + args = _arguments() + try: + result = asyncio.run(run(args)) + except ConfigurationError as error: + if not args.expect_unsupported: + raise + message = str(error) + if args.adcp_version not in message or "not advertised by this SDK" not in message: + raise RuntimeError("unsupported-version failure was not actionable") from error + result = { + "kind": "python_core_semantic_probe", + "package": {"name": "adcp", "version": version("adcp")}, + "protocol": args.adcp_version, + "status": "actionable_unsupported", + "phase": "client_construction", + "error": { + "name": type(error).__name__, + "message": message, + }, + "mutation_attempted": False, + } + else: + if args.expect_unsupported: + raise RuntimeError("expected an unsupported-version failure, but the request succeeded") + print(json.dumps(result, sort_keys=True, separators=(",", ":"))) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/reporting_interop/python_core_server.py b/scripts/ci/reporting_interop/python_core_server.py new file mode 100644 index 000000000..5a44b9af6 --- /dev/null +++ b/scripts/ci/reporting_interop/python_core_server.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +"""Installed-artifact Python Reliable Reporting Core MCP server. + +This process deliberately composes only public SDK primitives. It owns no +release-service lifecycle and makes no Managed Delivery or Reconciled Billing +claim. A runner must give every invocation a fresh PostgreSQL database. +""" + +from __future__ import annotations + +import argparse +import os +from datetime import datetime, timedelta, timezone +from typing import Any + +from psycopg_pool import AsyncConnectionPool + +from adcp.reporting.ledger import ( + PgReportingLedgerStore, + ReportingConfiguration, + ReportingDefinitionBinding, + ReportingObligationRecord, + ReportingRevisionRecord, + ReportingScheduleSpec, + ReportingStatusCaller, + ReportingStatusHandler, + derive_period, + revision_content_sha256, +) +from adcp.reporting.ledger.status_server import ReportingStatusNotificationHandler +from adcp.server import serve +from adcp.server.auth import BearerTokenAuth, Principal, auth_context_factory +from adcp.server.responses import capabilities_response, sync_accounts_response + +ADCP_VERSION = "3.2.0-rc.4" +WIRE_ADCP_VERSION = "3.2-rc.4" +FIXED_NOW = datetime(2026, 9, 3, tzinfo=timezone.utc) +PERIOD_START = datetime(2026, 8, 1, tzinfo=timezone.utc) +PERIOD_END = PERIOD_START + timedelta(days=1) +ACCOUNTS = ("interop-account-a", "interop-account-b") +TOKENS = { + "interop-token-a": (ACCOUNTS[0], "https://buyer-a.example.test/adcp"), + "interop-token-b": (ACCOUNTS[1], "opaque-consumer-b"), +} + + +def _configuration(account_id: str) -> ReportingConfiguration: + return ReportingConfiguration( + delivery_config_id="shared-daily-key", + delivery_config_version=1, + account_id=account_id, + report_definition_id="interop-core-v1", + reporting_profile="interop-core-v1", + feed_purpose="analytics", + schedule=ReportingScheduleSpec("P1D", "PT1H", period_anchor=PERIOD_START), + required_finality="snapshot", + activated_at=PERIOD_START, + deactivated_at=PERIOD_END, + media_buy_ids=(f"media-buy-{account_id[-1]}",), + definition=ReportingDefinitionBinding( + report_definition_uri="https://contracts.example.test/interop-core-v1.json", + report_definition_sha256="a" * 64, + schema_version="1", + schema_uri="https://contracts.example.test/interop-core-rows-v1.json", + schema_sha256="b" * 64, + ), + ) + + +def _obligation(configuration: ReportingConfiguration) -> ReportingObligationRecord: + period = derive_period(configuration.schedule, account_timezone="UTC", ordinal=0) + return ReportingObligationRecord( + reporting_obligation_id=f"interop-obligation-{configuration.account_id[-1]}", + account_id=configuration.account_id, + delivery_config_id=configuration.delivery_config_id, + delivery_config_version=configuration.delivery_config_version, + report_definition_id=configuration.report_definition_id, + reporting_profile=configuration.reporting_profile, + feed_purpose=configuration.feed_purpose, + period=period, + scope_resolved_at=period.end, + media_buy_ids=configuration.media_buy_ids, + required_finality=configuration.required_finality, + automated_recovery_deadline_at=period.expected_at + timedelta(days=1), + schedule=configuration.schedule, + definition=configuration.definition, + created_at=period.end, + currency="USD", + ) + + +def _revision( + obligation: ReportingObligationRecord, +) -> tuple[ReportingRevisionRecord, list[dict[str, Any]]]: + rows = [ + { + "media_buy_id": obligation.media_buy_ids[0], + "impressions": 5, + "vendor": {"fraction": "1.25", "enabled": True, "ordinal": 1}, + } + ] + revision_id = f"interop-revision-{obligation.account_id[-1]}" + totals = (("impressions", "5"),) + return ( + ReportingRevisionRecord( + reporting_revision_id=revision_id, + account_id=obligation.account_id, + reporting_obligation_id=obligation.reporting_obligation_id, + finality="snapshot", + revision_content_sha256=revision_content_sha256( + reporting_revision_id=revision_id, + row_count=len(rows), + control_totals=totals, + reporting_rows=rows, + ), + row_count=len(rows), + control_totals=totals, + observed_at=obligation.period.end, + data_through=obligation.period.end, + created_at=obligation.period.end, + ), + rows, + ) + + +class CoreHandler(ReportingStatusNotificationHandler): + """Truthful Core-only mount over the durable public ledger.""" + + advertised_tools = {"get_adcp_capabilities", "get_reporting_status", "sync_accounts"} + + async def sync_accounts(self, params: Any, context: Any = None) -> dict[str, Any]: + """Return the already-provisioned Core fixture account configuration. + + This makes the capability's schema-required configuration task real, + but the matrix does not credit this fixture method as typed production + onboarding evidence. + """ + request = ( + params.model_dump(mode="json", exclude_none=True) + if hasattr(params, "model_dump") + else dict(params) + ) + accounts: list[dict[str, Any]] = [] + for item in request.get("accounts", []): + account = item.get("account") if isinstance(item, dict) else None + account_id = account.get("account_id") if isinstance(account, dict) else None + if account_id in ACCOUNTS: + accounts.append( + { + "account": {"account_id": account_id}, + "action": "unchanged", + "status": "active", + "currency": "USD", + "timezone": "UTC", + } + ) + else: + accounts.append( + { + "account": {"account_id": account_id or "unknown"}, + "action": "failed", + "errors": [ + { + "code": "ACCOUNT_NOT_FOUND", + "message": "account is not part of this isolated fixture", + } + ], + } + ) + return sync_accounts_response(accounts, sandbox=False) + + async def get_adcp_capabilities(self, params: Any, context: Any = None) -> dict[str, Any]: + response = capabilities_response( + ["media_buy"], + sandbox=False, + idempotency={"supported": False}, + # capabilities_response normalizes the packaged three-component + # SDK version to the protocol's release-precision wire spelling. + adcp_version=ADCP_VERSION, + supported_versions=[WIRE_ADCP_VERSION], + ) + response["experimental_features"] = ["media_buy.reporting_delivery"] + response["media_buy"] = { + "reporting_delivery": { + "supported": True, + "configuration_task": "sync_accounts", + "status_task": "get_reporting_status", + "offerings": [ + { + "offering_id": "interop-core-daily", + "feed_purpose": "analytics", + "report_definition_id": "interop-core-v1", + "report_definition_uri": ( + "https://contracts.example.test/interop-core-v1.json" + ), + "report_definition_sha256": "a" * 64, + "reporting_profile": { + "id": "interop-core-v1", + "version": "1", + "schema_uri": ( + "https://contracts.example.test/interop-core-rows-v1.json" + ), + "schema_sha256": "b" * 64, + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "media_buy", + "primary_keys": ["media_buy_id"], + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT1H", + }, + "supported_finality": ["snapshot"], + "reconciliation_mode": "delivery_only", + } + ], + "automated_recovery_window_seconds": 86400, + "status_retention_days": 30, + } + } + return response + + +def _arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", required=True, type=int) + parser.add_argument("--database-url", default=os.environ.get("DATABASE_URL")) + return parser.parse_args() + + +def main() -> None: + args = _arguments() + if not args.database_url: + raise SystemExit("--database-url is required") + pool = AsyncConnectionPool( + args.database_url, + min_size=1, + max_size=4, + open=False, + kwargs={"autocommit": False}, + ) + store = PgReportingLedgerStore(pool=pool, clock=lambda: FIXED_NOW) + + async def startup() -> None: + await pool.open() + await pool.wait(timeout=15) + await store.create_schema() + for account_id in ACCOUNTS: + configuration = _configuration(account_id) + obligation = _obligation(configuration) + revision, rows = _revision(obligation) + await store.put_configuration(configuration) + await store.commit_obligation(obligation) + await store.commit_revision(revision, rows) + + async def shutdown() -> None: + await pool.close() + + async def resolve_caller(request: dict[str, Any], context: Any) -> ReportingStatusCaller: + if context is None or context.caller_identity is None: + raise PermissionError("authenticated reporting caller required") + requested = request.get("account") + account_id = requested.get("account_id") if isinstance(requested, dict) else None + authorized = context.metadata.get("account_id") + consumer_id = context.metadata.get("consumer_id") + if account_id != authorized or not isinstance(consumer_id, str): + raise PermissionError("caller is not authorized for this account") + return ReportingStatusCaller(account_id, consumer_id) + + def validate_token(token: str) -> Principal | None: + binding = TOKENS.get(token) + if binding is None: + return None + account_id, consumer_id = binding + return Principal( + caller_identity=f"principal-{account_id[-1]}", + tenant_id=f"tenant-{account_id[-1]}", + metadata={"account_id": account_id, "consumer_id": consumer_id}, + ) + + handler = CoreHandler( + ReportingStatusHandler(store), resolve_caller=resolve_caller, adcp_version=ADCP_VERSION + ) + serve( + handler, + name="reporting-interop-python-core", + host=args.host, + port=args.port, + transport="both", + auth=BearerTokenAuth(validate_token=validate_token), + context_factory=auth_context_factory, + stateless_http=True, + allowed_hosts=(args.host, "localhost", "127.0.0.1"), + on_startup=(startup,), + on_shutdown=(shutdown,), + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/reporting_interop/python_dependency_floor_probe.py b/scripts/ci/reporting_interop/python_dependency_floor_probe.py new file mode 100644 index 000000000..4d926c9d5 --- /dev/null +++ b/scripts/ci/reporting_interop/python_dependency_floor_probe.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Bind installed dependency floors and preserve historical reporting model reds.""" + +from __future__ import annotations + +import argparse +import hashlib +import importlib.metadata +import json +import sys +from decimal import Decimal +from pathlib import Path + +from adcp import ADCPClient +from adcp.reporting import ( + load_reporting_ledger, + plan_consumer_statuses, + post_consumer_statuses, + reconcile_reporting, + reconcile_reporting_core, +) +from adcp.reporting.feed.request import FeedRequest, transport_parameters +from adcp.types import ( + GetMediaBuyDeliveryRequest, + GetReportingStatusRequest, + ReportingDeliveryConfiguration, + SyncAccountsRequest, + SyncReportingReceiptsRequest, + SyncReportingStatusRequest, +) + + +def _arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--artifact", required=True, type=Path) + parser.add_argument("--expected-pydantic", required=True) + parser.add_argument("--expected-mcp", required=True) + return parser.parse_args() + + +def main() -> None: + args = _arguments() + artifact = args.artifact.resolve() + pydantic = importlib.metadata.version("pydantic") + mcp = importlib.metadata.version("mcp") + if (pydantic, mcp) != (args.expected_pydantic, args.expected_mcp): + raise SystemExit(f"unexpected dependency versions: pydantic={pydantic}, mcp={mcp}") + distribution = importlib.metadata.distribution("adcp") + direct_url = json.loads(distribution.read_text("direct_url.json") or "null") + if direct_url is None or direct_url.get("url") != artifact.as_uri(): + raise SystemExit("adcp was not installed from the supplied exact local artifact") + installed_members = { + item.as_posix(): hashlib.sha256(distribution.locate_file(item).read_bytes()).hexdigest() + for item in (distribution.files or []) + if item.as_posix().startswith("adcp/") and distribution.locate_file(item).is_file() + } + + scope_type = ReportingDeliveryConfiguration.model_fields["scope"].annotation + explicit_scope = scope_type.model_validate({"media_buy_ids": ["shared-media-buy"]}) + encoded_scope = explicit_scope.model_dump(mode="json", exclude_none=True) + exact_request = GetMediaBuyDeliveryRequest.model_validate( + { + "account": {"account_id": "usd"}, + "reporting_revision_id": "rpr_f1f83c2f27f7c8fe5fac9bb94abe3d8551e07ef5", + "pagination": {"max_results": 100}, + } + ) + encoded_exact = exact_request.model_dump(mode="json", exclude_none=True) + numeric_requests = [] + for value in ("9007199254740993.0", "9007199254740992.0"): + decoded = json.loads( + '{"view":"periods","account":{"account_id":"usd"},"ext":{"v":' + value + "}}", + parse_float=Decimal, + ) + numeric_requests.append(FeedRequest.parse(transport_parameters(decoded))) + + public_imports = { + item.__name__: True + for item in [ + ADCPClient, + SyncAccountsRequest, + GetReportingStatusRequest, + GetMediaBuyDeliveryRequest, + SyncReportingReceiptsRequest, + SyncReportingStatusRequest, + load_reporting_ledger, + reconcile_reporting, + reconcile_reporting_core, + plan_consumer_statuses, + post_consumer_statuses, + ] + } + report = { + "kind": "reporting_dependency_floor_probe", + "python": sys.version.split()[0], + "artifact": { + "path": str(artifact), + "sha256": hashlib.sha256(artifact.read_bytes()).hexdigest(), + "direct_url": direct_url, + }, + "installed_sdk_members": { + "count": len(installed_members), + "manifest_sha256": hashlib.sha256( + json.dumps(installed_members, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest(), + }, + "dependencies": {"pydantic": pydantic, "mcp": mcp}, + "requires_dist_pydantic": sorted( + item for item in (distribution.requires or []) if item.lower().startswith("pydantic") + ), + "public_imports": public_imports, + "reporting_model_controls": { + "py_public_001": { + "raw": {"media_buy_ids": ["shared-media-buy"]}, + "encoded": encoded_scope, + "required_correct": encoded_scope == {"media_buy_ids": ["shared-media-buy"]}, + }, + "py_public_002": { + "raw_has_breakdown_flags": False, + "encoded": encoded_exact, + "required_correct": "include_package_daily_breakdown" not in encoded_exact + and "include_window_breakdown" not in encoded_exact, + }, + "rlx_py_002": { + "bindings": [request.filters_json.decode() for request in numeric_requests], + "required_correct": numeric_requests[0].filters_json + != numeric_requests[1].filters_json, + }, + }, + "limitations": [ + "pr1208_artifact_metadata_still_predates_merged_main_1190_floor", + "model_serialization_and_import_control_only", + "not_production_or_quadrant_acceptance", + "does_not_transfer_to_integrated_main", + ], + } + if not all(item["required_correct"] for item in report["reporting_model_controls"].values()): + raise SystemExit("reporting model required-correct control failed") + print(json.dumps(report, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/reporting_interop/run_foundation_matrix.py b/scripts/ci/reporting_interop/run_foundation_matrix.py new file mode 100644 index 000000000..10fe70706 --- /dev/null +++ b/scripts/ci/reporting_interop/run_foundation_matrix.py @@ -0,0 +1,2582 @@ +#!/usr/bin/env python3 +"""Fresh-process/fresh-database staged reporting foundation runner. + +The current executable lane is the Python Core reference control. The result +document remains non-acceptance until every blocking language quadrant and +supported-skew cell has an implemented launcher and a semantic pass. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import secrets +import signal +import socket +import subprocess +import sys +import tarfile +import time +import zipfile +from collections import Counter +from collections.abc import Mapping +from dataclasses import dataclass +from pathlib import Path +from typing import Any +from urllib.parse import quote, urlencode + +import psycopg +from psycopg import sql +from psycopg.conninfo import conninfo_to_dict, make_conninfo + +ROOT = Path(__file__).resolve().parents[3] +HERE = Path(__file__).resolve().parent +PYTHON_SERVER = HERE / "python_core_server.py" +PYTHON_CLIENT = HERE / "python_core_client.py" +PYTHON_DEPENDENCY_FLOOR = HERE / "python_dependency_floor_probe.py" +TS_SERVER = HERE / "ts_core_server.cjs" +TS_CORE_BUYER = HERE / "ts_core_buyer.cjs" +TS_STORYBOARDS = HERE / "ts_storyboard_inventory.cjs" +STORYBOARD_ORCHESTRATION = HERE / "storyboard_orchestration.py" +OFFICIAL_PRECEDENCE = HERE / "official_precedence" / "repro-candidate.cjs" +OFFICIAL_CASES = HERE / "official_precedence" / "cases.json" +SIGNING_RUNNER = HERE / "signing" / "run-signing-vectors.cjs" +WEBHOOK_VECTORS_PYTHON = HERE / "signing" / "webhook_vectors_python.original.py.txt" +WEBHOOK_VECTORS_TYPESCRIPT = HERE / "signing" / "webhook_vectors_typescript.cjs" +WEBHOOK_REVOCATION_STATE = HERE / "signing" / "webhook_revocation_state.py" +WEBHOOK_SIGN_PYTHON = HERE / "signing" / "signer_roundtrip_python.original.py.txt" +WEBHOOK_VERIFY_TYPESCRIPT = HERE / "signing" / "crossverify_typescript.cjs" +WEBHOOK_SIGN_TYPESCRIPT = HERE / "signing" / "sign_webhook_typescript.cjs" +WEBHOOK_VERIFY_PYTHON = HERE / "signing" / "crossverify_python.py" +WEBHOOK_RECEIVER_SERVER = HERE / "signing" / "webhook_receiver_server.py" +WEBHOOK_RECEIVER_CLIENT = HERE / "signing" / "webhook_receiver_client.py" +WEBHOOK_VECTORS_AGGREGATE_SHA256 = ( + "b94b7597b3640d874406654a09296ffe4c6f333876bf9845919f133d49dfec52" +) +RESOURCE_SENTINELS = HERE / "security" / "resource_location_sentinels.json" +RESOURCE_PYTHON = HERE / "security" / "resource_location_python.original.py.txt" +RESOURCE_TYPESCRIPT = HERE / "security" / "resource_location_typescript.cjs" +APPLICABILITY = HERE / "applicability.json" +PINS = HERE / "pins.json" +TS_CORE_PRIVATE_ENV = { + "token_a": "ADCP_INTEROP_TS_CORE_AUTH_TOKEN_A", + "token_b": "ADCP_INTEROP_TS_CORE_AUTH_TOKEN_B", + "startup_proof": "ADCP_INTEROP_TS_CORE_STARTUP_PROOF", +} +NARROW_RUNTIME_ENV_KEYS = frozenset( + {"PATH", "LANG", "LC_ALL", "TZ", "TMPDIR", "TEMP", "TMP", "SYSTEMROOT", "WINDIR"} +) + + +class HarnessError(RuntimeError): + pass + + +@dataclass(frozen=True) +class CoreSellerCredentials: + token_a: str + token_b: str + startup_proof: str + + +@dataclass(frozen=True) +class PythonRuntime: + route: str + executable: Path + + +@dataclass(frozen=True) +class PythonArtifact: + route: str + path: Path + + +@dataclass(frozen=True) +class PythonDependencyRuntime: + label: str + executable: Path + pydantic: str + mcp: str + artifact_route: str + + +@dataclass(frozen=True) +class PreviousPythonInput: + executable: Path + artifact: Path + + +@dataclass(frozen=True) +class TypeScriptInstall: + role: str + root: Path + + @property + def lock(self) -> Path: + return self.root / "package-lock.json" + + +@dataclass(frozen=True) +class TypeScriptArchive: + role: str + path: Path + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _selected_python_build() -> dict[str, Any]: + candidate = json.loads(PINS.read_text(encoding="utf-8"))["python"]["candidate"] + for build in candidate["known_independent_builds"]: + if build["builder"] == "reporting_interop_harness": + return build + raise HarnessError("pins omit the reporting-interop harness build identity") + + +def _python_wheel_sdk_identity(path: Path) -> dict[str, Any]: + members: dict[str, str] = {} + try: + with zipfile.ZipFile(path) as archive: + for info in archive.infolist(): + if info.is_dir() or not info.filename.startswith("adcp/"): + continue + if info.filename in members: + raise HarnessError(f"duplicate SDK member in wheel: {info.filename}") + members[info.filename] = hashlib.sha256(archive.read(info)).hexdigest() + except zipfile.BadZipFile as error: + raise HarnessError(f"previous Python artifact is not a valid wheel: {path}") from error + if not members: + raise HarnessError(f"Python wheel contains no SDK members: {path}") + return { + "sdk_member_count": len(members), + "sdk_member_manifest_sha256": hashlib.sha256( + json.dumps(members, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest(), + } + + +def _expected_python_member_identity( + runtime: PythonRuntime, artifact: PythonArtifact +) -> dict[str, Any]: + if runtime.route != "previous": + selected = _selected_python_build() + return { + "source": "selected_candidate_build_manifest", + "sdk_member_count": selected["sdk_member_count"], + "sdk_member_manifest_sha256": selected["sdk_member_manifest_sha256"], + } + previous = json.loads(PINS.read_text(encoding="utf-8"))["python"]["previous_released"] + observed_hash = _sha256(artifact.path) + if observed_hash != previous["wheel_sha256"]: + raise HarnessError( + f"previous Python artifact does not match its immutable wheel pin: {observed_hash}" + ) + return { + "source": "exact_previous_wheel_members", + **_python_wheel_sdk_identity(artifact.path), + } + + +def _validate_python_artifact_inputs(artifacts: dict[str, PythonArtifact]) -> None: + build = _selected_python_build() + expected = { + "wheel": build["vcs_wheel_sha256"], + "sdist": build["sdist_sha256"], + } + for route, artifact in artifacts.items(): + observed = _sha256(artifact.path) + if observed != expected[route]: + raise HarnessError( + f"{route} artifact is not the selected independent harness build: {observed}" + ) + + +def _tree_aggregate_sha256(root: Path) -> tuple[str, dict[str, str]]: + entries: dict[str, str] = {} + aggregate = hashlib.sha256() + for path in sorted(item for item in root.rglob("*") if item.is_file()): + relative = f"./{path.relative_to(root).as_posix()}" + digest = _sha256(path) + entries[relative] = digest + aggregate.update(f"{digest} {relative}\n".encode()) + return aggregate.hexdigest(), entries + + +def _parse_runtime(value: str) -> PythonRuntime: + route, separator, raw_path = value.partition("=") + if not separator or route not in {"wheel", "sdist"}: + raise argparse.ArgumentTypeError("runtime must be wheel=/path or sdist=/path") + executable = Path(os.path.abspath(raw_path)) + if not executable.is_file(): + raise argparse.ArgumentTypeError(f"runtime executable is unavailable: {executable}") + return PythonRuntime(route, executable) + + +def _parse_artifact(value: str) -> PythonArtifact: + route, separator, raw_path = value.partition("=") + if not separator or route not in {"wheel", "sdist"}: + raise argparse.ArgumentTypeError("artifact must be wheel=/path or sdist=/path") + path = Path(os.path.abspath(raw_path)) + if not path.is_file(): + raise argparse.ArgumentTypeError(f"artifact is unavailable: {path}") + if route == "wheel" and path.suffix != ".whl": + raise argparse.ArgumentTypeError("wheel artifact must end in .whl") + if route == "sdist" and not path.name.endswith(".tar.gz"): + raise argparse.ArgumentTypeError("sdist artifact must end in .tar.gz") + return PythonArtifact(route, path) + + +def _parse_dependency_runtime(value: str) -> PythonDependencyRuntime: + label, separator, raw = value.partition("=") + parts = raw.split(",") + if not separator or not label or len(parts) != 4: + raise argparse.ArgumentTypeError( + "dependency runtime must be label=/python,PD_VERSION,MCP_VERSION,wheel|sdist" + ) + raw_path, pydantic, mcp, artifact_route = parts + executable = Path(os.path.abspath(raw_path)) + if not executable.is_file() or artifact_route not in {"wheel", "sdist"}: + raise argparse.ArgumentTypeError("invalid dependency runtime executable or artifact route") + return PythonDependencyRuntime(label, executable, pydantic, mcp, artifact_route) + + +def _parse_typescript_install(value: str) -> TypeScriptInstall: + role, separator, raw_path = value.partition("=") + if not separator or role not in {"candidate", "historical_candidate", "previous", "lead"}: + raise argparse.ArgumentTypeError( + "TypeScript install must be candidate=/path, historical_candidate=/path, " + "previous=/path, or lead=/path" + ) + root = Path(os.path.abspath(raw_path)) + if not (root / "package-lock.json").is_file(): + raise argparse.ArgumentTypeError(f"TypeScript package lock is unavailable: {root}") + return TypeScriptInstall(role, root) + + +def _parse_typescript_archive(value: str) -> TypeScriptArchive: + role, separator, raw_path = value.partition("=") + if not separator or role not in {"candidate", "historical_candidate", "previous", "lead"}: + raise argparse.ArgumentTypeError( + "TypeScript archive must be candidate=/path, historical_candidate=/path, " + "previous=/path, or lead=/path" + ) + path = Path(os.path.abspath(raw_path)) + if not path.is_file(): + raise argparse.ArgumentTypeError(f"TypeScript archive is unavailable: {path}") + return TypeScriptArchive(role, path) + + +def _arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--python-runtime", action="append", type=_parse_runtime, required=True) + parser.add_argument("--python-artifact", action="append", type=_parse_artifact, required=True) + parser.add_argument("--previous-python-runtime", type=Path) + parser.add_argument("--previous-python-artifact", type=Path) + parser.add_argument( + "--python-dependency-runtime", + action="append", + type=_parse_dependency_runtime, + default=[], + ) + parser.add_argument("--node-runtime", type=Path) + parser.add_argument( + "--typescript-install", action="append", type=_parse_typescript_install, default=[] + ) + parser.add_argument( + "--typescript-archive", action="append", type=_parse_typescript_archive, default=[] + ) + parser.add_argument("--pg-admin-url", required=True) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--keep-databases", action="store_true") + return parser.parse_args() + + +def _free_port() -> int: + with socket.socket() as listener: + listener.bind(("127.0.0.1", 0)) + return int(listener.getsockname()[1]) + + +def _runtime_identity(runtime: PythonRuntime, artifact: PythonArtifact) -> dict[str, Any]: + code = ( + "import hashlib,json,sys; " + "from importlib.metadata import distribution,distributions,version; " + "packages=sorted((d.metadata['Name'].lower(),d.version) for d in distributions() " + "if d.metadata['Name']); " + "dist=distribution('adcp'); direct_raw=dist.read_text('direct_url.json'); " + "members={p.as_posix():hashlib.sha256(dist.locate_file(p).read_bytes()).hexdigest() " + "for p in (dist.files or []) if p.as_posix().startswith('adcp/') " + "and dist.locate_file(p).is_file()}; " + "print(json.dumps({'python':sys.version.split()[0],'adcp':version('adcp')," + "'installed_distributions':packages,'direct_url':json.loads(direct_raw) " + "if direct_raw else None,'sdk_member_count':len(members)," + "'sdk_member_manifest_sha256':hashlib.sha256(json.dumps(members,sort_keys=True," + "separators=(',',':')).encode()).hexdigest()}))" + ) + completed = subprocess.run( + [str(runtime.executable), "-I", "-c", code], + check=True, + text=True, + capture_output=True, + timeout=30, + ) + identity = json.loads(completed.stdout) + if not str(identity["python"]).startswith("3.10."): + raise HarnessError(f"{runtime.route} runtime is not Python 3.10: {identity}") + expected_url = artifact.path.as_uri() + if identity.get("direct_url", {}).get("url") != expected_url: + raise HarnessError( + f"{runtime.route} runtime was not installed from the supplied local artifact path" + ) + expected_members = _expected_python_member_identity(runtime, artifact) + if ( + identity.get("sdk_member_count") != expected_members["sdk_member_count"] + or identity.get("sdk_member_manifest_sha256") + != expected_members["sdk_member_manifest_sha256"] + ): + raise HarnessError( + f"{runtime.route} installed SDK members differ from {expected_members['source']}" + ) + return { + **identity, + "route": runtime.route, + "executable": str(runtime.executable), + "executable_sha256": _sha256(runtime.executable), + "installed_member_expectation": expected_members, + "artifact": { + "path": str(artifact.path), + "filename": artifact.path.name, + "bytes": artifact.path.stat().st_size, + "sha256": _sha256(artifact.path), + }, + } + + +def _run_dependency_control( + runtime: PythonDependencyRuntime, + artifact: PythonArtifact, + *, + output: Path, +) -> dict[str, Any]: + report = _run_json( + [ + str(runtime.executable), + "-I", + str(PYTHON_DEPENDENCY_FLOOR), + "--artifact", + str(artifact.path), + "--expected-pydantic", + runtime.pydantic, + "--expected-mcp", + runtime.mcp, + ], + cwd=output, + env=os.environ.copy(), + ) + controls = report.get("reporting_model_controls", {}) + installed_members = report.get("installed_sdk_members", {}) + selected = _selected_python_build() + if ( + report.get("python") != "3.10.21" + or report.get("dependencies") != {"pydantic": runtime.pydantic, "mcp": runtime.mcp} + or controls.get("py_public_001", {}).get("required_correct") is not True + or controls.get("py_public_002", {}).get("required_correct") is not True + or controls.get("rlx_py_002", {}).get("required_correct") is not True + or installed_members.get("count") != selected["sdk_member_count"] + or installed_members.get("manifest_sha256") != selected["sdk_member_manifest_sha256"] + ): + raise HarnessError(f"Python dependency control changed for {runtime.label}") + path = output / f"python-dependency-{runtime.label}.json" + path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return { + "label": runtime.label, + "status": "required_correct_controls_passed", + "artifact_route": runtime.artifact_route, + "result": report, + "evidence": _retained(path, output), + } + + +def _git_identity() -> dict[str, Any]: + def git(*arguments: str) -> str: + result = subprocess.run( + ["git", *arguments], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + timeout=30, + ) + return result.stdout.strip() + + return { + "commit": git("rev-parse", "HEAD"), + "tree": git("rev-parse", "HEAD^{tree}"), + "dirty": bool(git("status", "--porcelain", "--untracked-files=all")), + } + + +def _database_url(admin_url: str, name: str) -> str: + fields = conninfo_to_dict(admin_url) + fields["dbname"] = name + return make_conninfo(**fields) + + +def _node_database_url(admin_url: str, name: str) -> str: + fields = conninfo_to_dict(admin_url) + user = quote(fields.pop("user", ""), safe="") + password = fields.pop("password", None) + credentials = user + if password is not None: + credentials += f":{quote(password, safe='')}" + if credentials: + credentials += "@" + host = fields.pop("host", "127.0.0.1") + port = fields.pop("port", "5432") + fields.pop("dbname", None) + query = urlencode(fields) + suffix = f"?{query}" if query else "" + return f"postgresql://{credentials}{host}:{port}/{quote(name, safe='')}{suffix}" + + +def _create_database(admin_url: str, name: str) -> dict[str, str]: + with psycopg.connect(admin_url, autocommit=True) as connection: + statement = sql.SQL( + "CREATE DATABASE {} TEMPLATE template0 ENCODING 'UTF8' LC_COLLATE 'C' LC_CTYPE 'C'" + ).format(sql.Identifier(name)) + connection.execute(statement) + url = _database_url(admin_url, name) + with psycopg.connect(url) as connection: + row = connection.execute( + "SELECT current_database(), pg_encoding_to_char(encoding), datcollate " + "FROM pg_database WHERE datname=current_database()" + ).fetchone() + version = connection.execute("SHOW server_version").fetchone() + if row != (name, "UTF8", "C") or version is None: + raise HarnessError(f"database identity is not UTF8/C: {row}") + return {"name": row[0], "encoding": row[1], "collation": row[2], "version": version[0]} + + +def _drop_database(admin_url: str, name: str) -> None: + with psycopg.connect(admin_url, autocommit=True) as connection: + connection.execute(sql.SQL("DROP DATABASE {} WITH (FORCE)").format(sql.Identifier(name))) + + +def _wait_port(process: subprocess.Popen[bytes], port: int, timeout: float = 30) -> None: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if process.poll() is not None: + raise HarnessError(f"seller exited during startup with {process.returncode}") + try: + with socket.create_connection(("127.0.0.1", port), timeout=0.2): + return + except OSError: + time.sleep(0.05) + raise HarnessError("seller did not bind its port before the deadline") + + +def _run_json(command: list[str], *, cwd: Path, env: dict[str, str]) -> dict[str, Any]: + completed = subprocess.run( + command, + cwd=cwd, + env=env, + text=True, + capture_output=True, + timeout=60, + ) + try: + result = json.loads(completed.stdout) + except json.JSONDecodeError as error: + raise HarnessError( + f"JSON probe emitted invalid output: {completed.stdout[:500]}" + ) from error + if completed.returncode != 0: + raise HarnessError( + f"JSON probe failed with {completed.returncode}: {result.get('error', result)}" + ) + return result + + +def _narrow_child_environment( + *, + private_bindings: Mapping[str, str], + configuration: Mapping[str, str] | None = None, + parent: Mapping[str, str] | None = None, +) -> dict[str, str]: + """Build an explicit child environment without forwarding ambient credentials.""" + + source = os.environ if parent is None else parent + environment = {key: value for key, value in source.items() if key in NARROW_RUNTIME_ENV_KEYS} + if configuration is not None: + environment.update(configuration) + environment.update(private_bindings) + return environment + + +def _buyer_environment(token: str, *, parent: Mapping[str, str] | None = None) -> dict[str, str]: + return _narrow_child_environment( + private_bindings={"ADCP_INTEROP_BUYER_TOKEN": token}, + parent=parent, + ) + + +def _typescript_pin(install: TypeScriptInstall) -> dict[str, Any]: + pins = json.loads(PINS.read_text(encoding="utf-8"))["typescript"] + key = { + "candidate": "development_candidate_rc47", + "historical_candidate": "historical_candidate_rc45", + "previous": "previous_compatible", + "lead": "newer_rc_lead", + }[install.role] + pin = pins[key] + if install.role == "candidate" and pin.get("executable_harness_input") is not True: + raise HarnessError( + "the rc.6 candidate is not selected as an executable lock/member binding; " + "historical rc.45 cannot substitute for required Q-cell execution" + ) + return pin + + +def _typescript_archive_identity( + install: TypeScriptInstall, archive_input: TypeScriptArchive +) -> dict[str, Any]: + """Bind the complete installed SDK tree to its exact immutable archive before use.""" + + if archive_input.role != install.role: + raise HarnessError("TypeScript archive and install roles differ") + pin = _typescript_pin(install) + archive_hash = _sha256(archive_input.path) + if archive_hash != pin["tarball_sha256"]: + raise HarnessError(f"{install.role} TypeScript archive hash differs from its immutable pin") + lock_hash = _sha256(install.lock) + if lock_hash != pin["package_lock_sha256"]: + raise HarnessError(f"{install.role} TypeScript lock differs from its immutable pin") + package_root = install.root / "node_modules/@adcp/sdk" + package_path = package_root / "package.json" + lock = json.loads(install.lock.read_text(encoding="utf-8")) + package = json.loads(package_path.read_text(encoding="utf-8")) + locked = lock.get("packages", {}).get("node_modules/@adcp/sdk", {}) + if ( + package.get("version") != pin["version"] + or locked.get("version") != pin["version"] + or locked.get("integrity") != pin["integrity"] + ): + raise HarnessError(f"{install.role} TypeScript install metadata differs from its pin") + archived: dict[str, str] = {} + try: + with tarfile.open(archive_input.path, "r:gz") as stream: + for member in stream.getmembers(): + if member.isdir(): + continue + if not member.isfile() or not member.name.startswith("package/"): + raise HarnessError(f"unsupported TypeScript archive member {member.name!r}") + relative = member.name.removeprefix("package/") + if ( + not relative + or relative.startswith("/") + or ".." in Path(relative).parts + or relative in archived + ): + raise HarnessError(f"unsafe or duplicate archive member {member.name!r}") + extracted = stream.extractfile(member) + if extracted is None: + raise HarnessError(f"cannot read archive member {member.name!r}") + archived[relative] = hashlib.sha256(extracted.read()).hexdigest() + except tarfile.TarError as error: + raise HarnessError(f"invalid TypeScript archive {archive_input.path}: {error}") from error + expected_members = pin.get("tarball_members") + if expected_members is not None and len(archived) != expected_members: + raise HarnessError(f"{install.role} archive member count differs from its immutable pin") + installed = { + path.relative_to(package_root).as_posix(): _sha256(path) + for path in sorted(package_root.rglob("*")) + if path.is_file() + } + if archived != installed: + missing = sorted(set(archived) - set(installed)) + unexpected = sorted(set(installed) - set(archived)) + changed = sorted( + name for name in set(archived) & set(installed) if archived[name] != installed[name] + ) + raise HarnessError( + f"{install.role} installed SDK differs from its archive: " + f"missing={missing[:5]} unexpected={unexpected[:5]} changed={changed[:5]}" + ) + manifest = hashlib.sha256( + json.dumps(archived, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest() + return { + "role": install.role, + "version": pin["version"], + "integrity": pin["integrity"], + "archive": str(archive_input.path), + "archive_bytes": archive_input.path.stat().st_size, + "archive_sha256": archive_hash, + "package_lock": str(install.lock), + "package_lock_sha256": lock_hash, + "installed_member_count": len(installed), + "installed_member_manifest_sha256": manifest, + } + + +def _node_identity(node: Path) -> dict[str, Any]: + completed = subprocess.run( + [ + str(node), + "-p", + "JSON.stringify({node:process.version,platform:process.platform,arch:process.arch})", + ], + check=True, + capture_output=True, + text=True, + timeout=30, + ) + result = json.loads(completed.stdout) + if result["node"] != "v22.12.0": + raise HarnessError(f"interoperability floor requires Node v22.12.0: {result}") + return {**result, "executable": str(node), "executable_sha256": _sha256(node)} + + +def _ts_core_buyer_command( + node: Path, + install: TypeScriptInstall, + *, + endpoint: str, + expect_missing: bool, +) -> list[str]: + pin = _typescript_pin(install) + command = [ + str(node), + str(TS_CORE_BUYER), + "--package-lock", + str(install.lock), + "--expected-version", + pin["version"], + "--expected-integrity", + pin["integrity"], + "--adcp-version", + "3.2.0-rc.4", + "--endpoint", + endpoint, + "--auth-env", + "ADCP_INTEROP_BUYER_TOKEN", + "--account", + "interop-account-a", + ] + if expect_missing: + command.append("--expect-missing-core-api") + return command + + +def _storyboard_inventory_command(node: Path, install: TypeScriptInstall) -> list[str]: + pin = _typescript_pin(install) + return [ + str(node), + str(TS_STORYBOARDS), + "--package-lock", + str(install.lock), + "--expected-version", + pin["version"], + "--expected-integrity", + pin["integrity"], + ] + + +def _ts_server_command( + node: Path, + install: TypeScriptInstall, + *, + port: int, + database_url: str, + ready_file: Path, +) -> list[str]: + pin = _typescript_pin(install) + return [ + str(node), + str(TS_SERVER), + "--package-lock", + str(install.lock), + "--expected-version", + pin["version"], + "--expected-integrity", + pin["integrity"], + "--adcp-version", + "3.2.0-rc.4", + "--host", + "127.0.0.1", + "--port", + str(port), + "--database-url", + database_url, + "--ready-file", + str(ready_file), + "--seller-role", + install.role, + ] + + +def _ts_server_environment(credentials: CoreSellerCredentials) -> dict[str, str]: + """Pass private inputs outside argv; this is not a same-user secrecy boundary.""" + + return _narrow_child_environment( + private_bindings={ + TS_CORE_PRIVATE_ENV["token_a"]: credentials.token_a, + TS_CORE_PRIVATE_ENV["token_b"]: credentials.token_b, + TS_CORE_PRIVATE_ENV["startup_proof"]: credentials.startup_proof, + }, + ) + + +def _start_ts_core_server( + node: Path, + install: TypeScriptInstall, + *, + port: int, + database_url: str, + credentials: CoreSellerCredentials, + ready_file: Path, + cwd: Path, + stdout: Any, + stderr: Any, +) -> subprocess.Popen[bytes]: + return subprocess.Popen( + _ts_server_command( + node, + install, + port=port, + database_url=database_url, + ready_file=ready_file, + ), + cwd=cwd, + env=_ts_server_environment(credentials), + stdin=subprocess.DEVNULL, + stdout=stdout, + stderr=stderr, + start_new_session=True, + ) + + +def _new_core_seller_credentials() -> CoreSellerCredentials: + """Generate execution credentials; readiness records retain digests only.""" + + credentials = CoreSellerCredentials( + token_a=secrets.token_urlsafe(32), + token_b=secrets.token_urlsafe(32), + startup_proof=secrets.token_hex(32), + ) + if ( + min(len(credentials.token_a), len(credentials.token_b)) < 32 + or len(credentials.startup_proof) < 32 + or len({credentials.token_a, credentials.token_b, credentials.startup_proof}) != 3 + ): + raise HarnessError("seller credential generator returned low-entropy or repeated values") + return credentials + + +def _linux_process_start_token(pid: int) -> str: + if not sys.platform.startswith("linux"): + raise HarnessError("Core seller owned-start proof currently requires Linux /proc") + try: + stat = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8") + except OSError as error: + raise HarnessError("could not read Core seller process identity") from error + command_end = stat.rfind(")") + fields = stat[command_end + 2 :].split() if command_end >= 0 else [] + if len(fields) <= 19 or not fields[19].isdigit(): + raise HarnessError("Core seller process identity is malformed") + return fields[19] + + +def _wait_core_seller_ready( + process: subprocess.Popen[bytes], + ready_file: Path, + *, + node: Path, + install: TypeScriptInstall, + credentials: CoreSellerCredentials, + port: int, + timeout: float = 60, +) -> dict[str, Any]: + expected_pin = _typescript_pin(install) + expected_start = _linux_process_start_token(process.pid) + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if process.poll() is not None: + raise HarnessError( + f"TypeScript Core seller exited before readiness: {process.returncode}" + ) + if ready_file.is_file(): + try: + ready = json.loads(ready_file.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise HarnessError( + "TypeScript Core seller readiness record is malformed" + ) from error + expected_auth = { + "interop-account-a": hashlib.sha256(credentials.token_a.encode()).hexdigest(), + "interop-account-b": hashlib.sha256(credentials.token_b.encode()).hexdigest(), + } + package = ready.get("seller_package", {}) + runtime = ready.get("node", {}) + if ( + ready.get("kind") != "reporting_interop_ts_core_server_ready" + or ready.get("pid") != process.pid + or ready.get("port") != port + or ready.get("process_start_token") != expected_start + or ready.get("startup_proof_sha256") + != hashlib.sha256(credentials.startup_proof.encode()).hexdigest() + or ready.get("auth_binding_sha256") != expected_auth + or Path(runtime.get("executable", "")).resolve() != node.resolve() + or runtime.get("version") != "v22.12.0" + or package.get("name") != expected_pin.get("package", "@adcp/sdk") + or package.get("language") != "typescript" + or package.get("package_role") != install.role + or package.get("version") != expected_pin["version"] + or package.get("integrity") != expected_pin["integrity"] + or package.get("protocol") != expected_pin["protocol"] + ): + raise HarnessError( + "TypeScript Core seller readiness identity did not match the selected seller" + ) + return ready + time.sleep(0.05) + raise HarnessError("TypeScript Core seller did not publish owned readiness before timeout") + + +def _python_core_client_command( + executable: Path, + *, + endpoint: str, + expect_unsupported: bool = False, +) -> list[str]: + command = [ + str(executable), + "-I", + str(PYTHON_CLIENT), + "--url", + endpoint, + "--auth-env", + "ADCP_INTEROP_BUYER_TOKEN", + "--account", + "interop-account-a", + "--adcp-version", + "3.2.0-rc.4", + ] + if expect_unsupported: + command.append("--expect-unsupported") + return command + + +def _run_official_precedence( + node: Path, install: TypeScriptInstall, output: Path +) -> dict[str, Any]: + if install.role != "candidate": + raise HarnessError("official-precedence blocking probe must use the candidate TS pin") + result_path = output / "official-precedence-candidate.json" + stderr_path = output / "official-precedence-candidate.stderr.log" + completed = subprocess.run( + [str(node), str(OFFICIAL_PRECEDENCE), str(result_path)], + cwd=output, + env={**os.environ, "NODE_PATH": str(install.root / "node_modules")}, + text=True, + capture_output=True, + timeout=60, + ) + stderr_path.write_text(completed.stderr, encoding="utf-8") + if completed.returncode != 0 or not result_path.is_file(): + raise HarnessError( + f"official-precedence probe did not produce its required semantic pass: " + f"exit={completed.returncode}" + ) + report = json.loads(result_path.read_text(encoding="utf-8")) + totals = report.get("totals", {}) + if ( + report.get("allInputsValid") is not True + or report.get("semanticCompatibilityPassed") is not True + or totals.get("semanticPass") != 17 + or totals.get("semanticFail") != 0 + ): + raise HarnessError("official-precedence candidate result is not required-correct") + manifest = json.loads(OFFICIAL_CASES.read_text(encoding="utf-8")) + expected_cases = manifest.get("cases", []) + observed_cases = [ + {"id": row.get("caseId"), "required": row.get("required")} + for row in report.get("cases", []) + ] + if ( + manifest.get("protocol_owned") is not False + or manifest.get("scope") != "supplemental_shared_neutral_regression" + or observed_cases != expected_cases + ): + raise HarnessError("official-precedence case identities or expectations changed") + return { + "status": "passed", + "corpus": { + "protocol_owned": False, + "manifest": _retained(OFFICIAL_CASES, ROOT), + }, + "result": report, + "evidence": [ + _retained(result_path, output), + _retained(stderr_path, output), + ], + } + + +def _run_signing_vectors( + node: Path, + install: TypeScriptInstall, + *, + admin_url: str, + output: Path, + database: str, +) -> dict[str, Any]: + database_identity = _create_database(admin_url, database) + database_url = _database_url(admin_url, database) + node_path = str(install.root / "node_modules") + migration = subprocess.run( + [ + str(node), + "-e", + "process.stdout.write(require('@adcp/sdk/signing/server').REPLAY_CACHE_MIGRATION)", + ], + env={**os.environ, "NODE_PATH": node_path}, + check=True, + capture_output=True, + text=True, + timeout=30, + ).stdout + with psycopg.connect(database_url, autocommit=True) as connection: + connection.execute(migration) + result_path = output / "signing-vectors.json" + stderr_path = output / "signing-vectors.stderr.log" + completed = subprocess.run( + [str(node), str(SIGNING_RUNNER), str(result_path)], + cwd=output, + env={ + **os.environ, + "NODE_PATH": node_path, + "PROBE_PORT": str(_free_port()), + "PROBE_PG_URL": _node_database_url(admin_url, database), + }, + text=True, + capture_output=True, + timeout=60, + ) + stderr_path.write_text(completed.stderr, encoding="utf-8") + if completed.returncode != 0 or not result_path.is_file(): + raise HarnessError(f"signing vectors failed with exit {completed.returncode}") + report = json.loads(result_path.read_text(encoding="utf-8")) + if report.get("totals") != {"vectors": 13, "passed": 13, "failed": 0}: + raise HarnessError(f"signing vector totals changed: {report.get('totals')}") + return { + "status": "passed", + "database": database_identity, + "result": report, + "evidence": [ + _retained(result_path, output), + _retained(stderr_path, output), + ], + } + + +def _python_webhook_vector_dir(runtime: PythonRuntime) -> Path: + completed = subprocess.run( + [ + str(runtime.executable), + "-I", + "-c", + "import adcp,pathlib; print(pathlib.Path(adcp.__file__).parent / " + "'_compliance/3.2.0-rc.4/test-vectors/webhook-signing')", + ], + check=True, + capture_output=True, + text=True, + timeout=30, + ) + path = Path(completed.stdout.strip()) + if not path.is_dir(): + raise HarnessError(f"Python rc.4 webhook vectors are unavailable: {path}") + return path + + +def _run_protocol_webhook_vectors( + runtimes: list[PythonRuntime], + artifacts: dict[str, PythonArtifact], + *, + node: Path, + install: TypeScriptInstall, + output: Path, +) -> dict[str, Any]: + python_results: list[dict[str, Any]] = [] + reference_entries: dict[str, str] | None = None + for runtime in runtimes: + vector_dir = _python_webhook_vector_dir(runtime) + aggregate, entries = _tree_aggregate_sha256(vector_dir) + if aggregate != WEBHOOK_VECTORS_AGGREGATE_SHA256: + raise HarnessError(f"Python webhook vector aggregate changed for {runtime.route}") + if reference_entries is None: + reference_entries = entries + elif entries != reference_entries: + raise HarnessError("Python webhook vector trees differ across artifact routes") + rows = _run_json( + [str(runtime.executable), "-I", str(WEBHOOK_VECTORS_PYTHON), str(vector_dir)], + cwd=output, + env=os.environ.copy(), + ) + mismatches = [row["vector"] for row in rows if row.get("match") is not True] + if len(rows) != 29 or mismatches != ["negative/019-revocation-stale.json"]: + raise HarnessError( + f"Python webhook conformance changed for {runtime.route}: {mismatches}" + ) + result_path = output / f"webhook-vectors-{runtime.route}.json" + result_path.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n", encoding="utf-8") + revocation_state = _run_json( + [str(runtime.executable), "-I", str(WEBHOOK_REVOCATION_STATE), str(vector_dir)], + cwd=output, + env=os.environ.copy(), + ) + if revocation_state.get("fresh") != {"success": True} or revocation_state.get("stale") != { + "error_code": "webhook_signature_revocation_stale", + "step": 9, + "success": False, + }: + raise HarnessError( + f"Python public revocation-list state control failed for {runtime.route}" + ) + revocation_path = output / f"webhook-revocation-state-{runtime.route}.json" + revocation_path.write_text( + json.dumps(revocation_state, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + python_results.append( + { + "route": runtime.route, + "runtime": _runtime_identity(runtime, artifacts[runtime.route]), + "totals": {"vectors": 29, "matched": 28, "mismatched": 1}, + "mismatch_ids": mismatches, + "rows": rows, + "revocation_state": revocation_state, + "evidence": [ + _retained(result_path, output), + _retained(revocation_path, output), + ], + } + ) + + if any(item["rows"] != python_results[0]["rows"] for item in python_results[1:]): + raise HarnessError("Python webhook results differ across artifact routes") + + ts_vector_dir = ( + install.root + / "node_modules/@adcp/sdk/compliance/cache/3.2.0-rc.4/test-vectors/webhook-signing" + ) + if not ts_vector_dir.is_dir(): + raise HarnessError("TypeScript rc.4 webhook vectors are unavailable") + ts_aggregate, ts_entries = _tree_aggregate_sha256(ts_vector_dir) + if ( + ts_aggregate != WEBHOOK_VECTORS_AGGREGATE_SHA256 + or reference_entries is None + or ts_entries != reference_entries + ): + raise HarnessError("installed Python and TypeScript webhook vector trees differ") + ts_rows = _run_json( + [ + str(node), + str(WEBHOOK_VECTORS_TYPESCRIPT), + str(install.root), + str(ts_vector_dir), + ], + cwd=output, + env=os.environ.copy(), + ) + ts_mismatches = [row["vector"] for row in ts_rows if row.get("match") is not True] + if len(ts_rows) != 29 or ts_mismatches != ["negative/019-revocation-stale.json"]: + raise HarnessError(f"TypeScript webhook conformance changed: {ts_mismatches}") + + ts_path = output / "webhook-vectors-typescript.json" + ts_path.write_text(json.dumps(ts_rows, indent=2, sort_keys=True) + "\n", encoding="utf-8") + python_by_id = {row["vector"]: row["got"] for row in python_results[0]["rows"]} + ts_by_id = {row["vector"]: row["got"] for row in ts_rows} + disagreements = sorted( + vector for vector in python_by_id if python_by_id[vector] != ts_by_id[vector] + ) + if disagreements: + raise HarnessError(f"cross-SDK webhook outcomes changed: {disagreements}") + return { + "status": "verifier_corpus_28_of_29_receiver_contract_required", + "corpus": { + "owner": "protocol_3.2.0_rc4", + "aggregate_sha256": WEBHOOK_VECTORS_AGGREGATE_SHA256, + "files": len(ts_entries), + "trees_byte_identical": True, + }, + "inputs": { + "python_probe": _retained(WEBHOOK_VECTORS_PYTHON, ROOT), + "typescript_probe": _retained(WEBHOOK_VECTORS_TYPESCRIPT, ROOT), + "revocation_state_probe": _retained(WEBHOOK_REVOCATION_STATE, ROOT), + }, + "python": python_results, + "typescript": { + "runtime": _node_identity(node), + "pin": _typescript_pin(install), + "totals": {"vectors": 29, "matched": 28, "mismatched": 1}, + "mismatch_ids": ts_mismatches, + "rows": ts_rows, + "evidence": _retained(ts_path, output), + }, + "cross_sdk": { + "agreed": 29, + "disagreed": 0, + "disagreement_ids": disagreements, + "bare_verifier_normative_acceptance": False, + }, + "receiver_contract": { + "status": "incomplete", + "required_vector": "negative/019-revocation-stale.json", + "requires_contract": "webhook_receiver_runner", + "fault": "simulate_stale_revocation_fetch", + "configured_public_revocation_list_control": "passed_both_python_artifact_routes", + "remaining": [ + "satisfied_only_by_the_separate_live_receiver_finding", + ], + }, + "limitations": [ + "in_process_public_verifiers", + "not_socket_callback_evidence", + "not_retry_or_activity_evidence", + ], + } + + +def _run_cross_language_webhook_signing( + runtimes: list[PythonRuntime], + artifacts: dict[str, PythonArtifact], + *, + node: Path, + install: TypeScriptInstall, + output: Path, +) -> dict[str, Any]: + ts_vector_dir = ( + install.root + / "node_modules/@adcp/sdk/compliance/cache/3.2.0-rc.4/test-vectors/webhook-signing" + ) + python_rows: list[dict[str, Any]] = [] + for runtime in runtimes: + vector_dir = _python_webhook_vector_dir(runtime) + sample = _run_json( + [str(runtime.executable), "-I", str(WEBHOOK_SIGN_PYTHON), str(vector_dir)], + cwd=output, + env=os.environ.copy(), + ) + if sample.get( + "signature_alphabet" + ) != "url(-_)" or "_private_d_for_test_only" in json.dumps(sample): + raise HarnessError(f"Python webhook signer output changed for {runtime.route}") + sample_path = output / f"webhook-signed-python-{runtime.route}.json" + sample_path.write_text( + json.dumps(sample, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + python_self = _run_json( + [ + str(runtime.executable), + "-I", + str(WEBHOOK_VERIFY_PYTHON), + str(sample_path), + str(vector_dir), + ], + cwd=output, + env=os.environ.copy(), + ) + typescript_cross = _run_json( + [ + str(node), + str(WEBHOOK_VERIFY_TYPESCRIPT), + str(install.root), + str(sample_path), + str(ts_vector_dir), + ], + cwd=output, + env=os.environ.copy(), + ) + if python_self != {"accepted": True} or ( + typescript_cross.get("ts_accepts_python_signed_webhook") is not True + ): + raise HarnessError(f"Python-signed webhook 2x2 outcome changed for {runtime.route}") + python_rows.append( + { + "route": runtime.route, + "runtime": _runtime_identity(runtime, artifacts[runtime.route]), + "emitted": sample["emitted_headers"], + "signature_alphabet": sample["signature_alphabet"], + "content_digest_alphabet": sample["content_digest_alphabet"], + "python_verifier": python_self, + "typescript_verifier": typescript_cross, + "evidence": _retained(sample_path, output), + } + ) + if any(row["emitted"] != python_rows[0]["emitted"] for row in python_rows[1:]): + raise HarnessError("Python webhook signer output differs across artifact routes") + + ts_sample = _run_json( + [str(node), str(WEBHOOK_SIGN_TYPESCRIPT), str(install.root), str(ts_vector_dir)], + cwd=output, + env=os.environ.copy(), + ) + expected_ts_signature = ( + "sig1=:iaoTUFbGl4Ka6O3G2H3mOxvFDhJNXSrlFNBsdDFKmvlNz1vr9jilnh6LBDFuCD4wY1g1rC9HxI9" + "lgRy_-04EBw:" + ) + if ts_sample.get("emitted", {}).get( + "Signature" + ) != expected_ts_signature or "_private_d_for_test_only" in json.dumps(ts_sample): + raise HarnessError("TypeScript webhook signer output changed") + ts_sample_path = output / "webhook-signed-typescript.json" + ts_sample_path.write_text( + json.dumps(ts_sample, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + ts_self = _run_json( + [ + str(node), + str(WEBHOOK_VERIFY_TYPESCRIPT), + str(install.root), + str(ts_sample_path), + str(ts_vector_dir), + ], + cwd=output, + env=os.environ.copy(), + ) + python_cross: list[dict[str, Any]] = [] + for runtime in runtimes: + result = _run_json( + [ + str(runtime.executable), + "-I", + str(WEBHOOK_VERIFY_PYTHON), + str(ts_sample_path), + str(_python_webhook_vector_dir(runtime)), + ], + cwd=output, + env=os.environ.copy(), + ) + python_cross.append({"route": runtime.route, "result": result}) + if ts_self.get("ts_accepts_python_signed_webhook") is not True or any( + row["result"] != {"accepted": True} for row in python_cross + ): + raise HarnessError("TypeScript-signed webhook 2x2 outcome changed") + + return { + "status": "passed", + "protocol_adjudication": { + "source_commit": "94976657c8456e5ad6de55d9793a883542a4fc5f", + "signature_profile": "unpadded_base64url", + "content_digest": "separately_bounded_wording_vector_discrepancy", + }, + "inputs": { + "python_signer_probe": _retained(WEBHOOK_SIGN_PYTHON, ROOT), + "python_verifier_probe": _retained(WEBHOOK_VERIFY_PYTHON, ROOT), + "typescript_signer_probe": _retained(WEBHOOK_SIGN_TYPESCRIPT, ROOT), + "typescript_verifier_probe": _retained(WEBHOOK_VERIFY_TYPESCRIPT, ROOT), + }, + "python_signer": python_rows, + "typescript_signer": { + "runtime": _node_identity(node), + "pin": _typescript_pin(install), + "emitted": ts_sample["emitted"], + "typescript_verifier": ts_self, + "python_verifiers": python_cross, + "evidence": _retained(ts_sample_path, output), + }, + "directions": { + "python_to_python": "accepted", + "python_to_typescript": "accepted", + "typescript_to_python": "accepted", + "typescript_to_typescript": "accepted", + }, + "semantic_compatibility": True, + "limitations": [ + "in_process_public_signers_and_verifiers", + "published_protocol_test_key_only", + "not_real_socket_callback_evidence", + ], + } + + +def _run_resource_location_comparison( + runtimes: list[PythonRuntime], + artifacts: dict[str, PythonArtifact], + *, + node: Path, + install: TypeScriptInstall, + output: Path, +) -> dict[str, Any]: + python_results: list[dict[str, Any]] = [] + for runtime in runtimes: + result = _run_json( + [str(runtime.executable), "-I", str(RESOURCE_PYTHON), str(RESOURCE_SENTINELS)], + cwd=output, + env=os.environ.copy(), + ) + if not isinstance(result, list) or len(result) != 17: + raise HarnessError( + f"resource-location Python sentinel result changed for {runtime.route}" + ) + result_path = output / f"resource-location-{runtime.route}.json" + result_path.write_text( + json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + python_results.append( + { + "route": runtime.route, + "runtime": _runtime_identity(runtime, artifacts[runtime.route]), + "rows": result, + "evidence": _retained(result_path, output), + } + ) + + typescript = _run_json( + [str(node), str(RESOURCE_TYPESCRIPT), str(install.root), str(RESOURCE_SENTINELS)], + cwd=output, + env=os.environ.copy(), + ) + if typescript.get("available") != "function" or len(typescript.get("rows", [])) != 17: + raise HarnessError("resource-location TypeScript sentinel guard is unavailable or changed") + typescript_path = output / "resource-location-typescript.json" + typescript_path.write_text( + json.dumps(typescript, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + + canonical_python = python_results[0]["rows"] + if any(item["rows"] != canonical_python for item in python_results[1:]): + raise HarnessError("resource-location Python artifact routes disagree") + python_by_id = {row["id"]: row["python"] for row in canonical_python} + ts_by_id = {row["id"]: row["ts"] for row in typescript["rows"]} + if set(python_by_id) != set(ts_by_id): + raise HarnessError("resource-location sentinel identities disagree") + divergences = sorted( + row_id for row_id in python_by_id if python_by_id[row_id] != ts_by_id[row_id] + ) + expected_divergences = sorted( + [ + "aws-key-id", + "bare-jwt", + "double-pct-encoded", + "http-plain", + "https-clean", + "pct-encoded-query", + "protocol-relative", + ] + ) + if divergences != expected_divergences: + raise HarnessError( + "resource-location native policy comparison changed and requires re-adjudication" + ) + return { + "id": "typescript_candidate_resource_location_guard_advisory", + "status": "advisory_observed", + "blocking": False, + "acceptance_effect": "none", + "evidence_scope": "in_process_installed_seller_persistence_guards_only", + "does_not_override_secret_leak_failure": True, + "pin": _typescript_pin(install), + "typescript_install": { + "package_lock": str(install.lock), + "package_lock_sha256": _sha256(install.lock), + }, + "inputs": { + "corpus": _retained(RESOURCE_SENTINELS, ROOT), + "python_probe": _retained(RESOURCE_PYTHON, ROOT), + "typescript_probe": _retained(RESOURCE_TYPESCRIPT, ROOT), + }, + "totals": {"sentinels": 17, "agree": 10, "diverge": 7}, + "divergence_ids": divergences, + "python": python_results, + "typescript": { + "runtime": _node_identity(node), + "rows": typescript["rows"], + "evidence": _retained(typescript_path, output), + }, + "limitations": [ + "not_real_http", + "not_mcp_quadrant_evidence", + "seller_persistence_guards_only", + "corpus_expect_field_is_not_a_pass_criterion", + ], + } + + +def _stop(process: subprocess.Popen[bytes]) -> None: + if process.poll() is not None: + return + try: + os.killpg(process.pid, signal.SIGTERM) + process.wait(timeout=10) + except subprocess.TimeoutExpired: + os.killpg(process.pid, signal.SIGKILL) + process.wait(timeout=5) + + +def _retained(path: Path, root: Path) -> dict[str, Any]: + return { + "path": str(path.relative_to(root)), + "bytes": path.stat().st_size, + "sha256": _sha256(path), + } + + +def _run_python_control( + runtime: PythonRuntime, + artifact: PythonArtifact, + *, + admin_url: str, + output: Path, + database: str, + node: Path | None, + candidate_ts: TypeScriptInstall | None, +) -> list[dict[str, Any]]: + database_identity = _create_database(admin_url, database) + database_url = _node_database_url(admin_url, database) + port = _free_port() + stdout_path = output / "seller.stdout.log" + stderr_path = output / "seller.stderr.log" + client_path = output / "client.json" + with stdout_path.open("wb") as seller_stdout, stderr_path.open("wb") as seller_stderr: + process = subprocess.Popen( + [ + str(runtime.executable), + "-I", + str(PYTHON_SERVER), + "--port", + str(port), + "--database-url", + database_url, + ], + cwd=output, + stdin=subprocess.DEVNULL, + stdout=seller_stdout, + stderr=seller_stderr, + start_new_session=True, + ) + try: + _wait_port(process, port) + completed = subprocess.run( + [ + str(runtime.executable), + "-I", + str(PYTHON_CLIENT), + "--url", + f"http://127.0.0.1:{port}/mcp", + "--auth-env", + "ADCP_INTEROP_BUYER_TOKEN", + "--account", + "interop-account-a", + "--adcp-version", + "3.2.0-rc.4", + ], + cwd=output, + text=True, + capture_output=True, + timeout=60, + env=_buyer_environment("interop-token-a"), + ) + client_path.write_text(completed.stdout, encoding="utf-8") + if completed.stderr: + (output / "client.stderr.log").write_text(completed.stderr, encoding="utf-8") + if completed.returncode != 0: + raise HarnessError(f"Python semantic buyer failed with {completed.returncode}") + result = json.loads(completed.stdout) + if result.get("definitive") is not True or result.get("submitted_receipts") != 0: + raise HarnessError(f"Python semantic result is not definitive Core: {result}") + ts_gap = None + if node is not None and candidate_ts is not None: + ts_gap = _run_json( + _ts_core_buyer_command( + node, + candidate_ts, + endpoint=f"http://127.0.0.1:{port}/mcp", + expect_missing=False, + ), + cwd=output, + env=_buyer_environment("interop-token-a"), + ) + finally: + _stop(process) + cells = [ + { + "id": "supplemental_shared_rc4__candidate_py_client__candidate_py_server", + "target_contract_id": "Q1", + "target_cell_id": "candidate_py_client__candidate_py_server", + "control_classification": "historical_rc4_shared_seller_control", + "required_cell_credit": False, + "execution_attempted": True, + "observed_protocol": "3.2.0-rc.4", + "shared_seller_database": True, + "status": "partial_pass", + "cell_complete": False, + "acceptance": False, + "reason": ( + "Core control passed; the cell's full declared scenario set is not implemented" + ), + "database": database_identity, + "runtime": _runtime_identity(runtime, artifact), + "result": result, + "entrypoints": { + "server": { + "path": str(PYTHON_SERVER.relative_to(ROOT)), + "sha256": _sha256(PYTHON_SERVER), + }, + "client": { + "path": str(PYTHON_CLIENT.relative_to(ROOT)), + "sha256": _sha256(PYTHON_CLIENT), + }, + }, + "evidence": [ + _retained(stdout_path, output), + _retained(stderr_path, output), + _retained(client_path, output), + ], + } + ] + if ts_gap is not None: + cells.append( + { + "id": "supplemental_shared_rc4__candidate_ts_client__candidate_py_server", + "target_contract_id": "Q2", + "target_cell_id": "candidate_ts_client__candidate_py_server", + "control_classification": "historical_rc4_shared_seller_control", + "required_cell_credit": False, + "execution_attempted": True, + "observed_protocol": "3.2.0-rc.4", + "shared_seller_database": True, + "status": "partial_pass", + "cell_complete": False, + "acceptance": False, + "reason": ( + "Candidate TypeScript Core reconciliation passed; the cell's full declared " + "scenario set remains incomplete" + ), + "result": ts_gap, + } + ) + return cells + + +def _run_ts_core_control( + runtime: PythonRuntime, + artifact: PythonArtifact, + *, + node: Path, + install: TypeScriptInstall, + admin_url: str, + output: Path, + database: str, +) -> list[dict[str, Any]]: + database_identity = _create_database(admin_url, database) + database_url = _node_database_url(admin_url, database) + pin = _typescript_pin(install) + port = _free_port() + stdout_path = output / "seller.stdout.log" + stderr_path = output / "seller.stderr.log" + client_path = output / "python-client.json" + ts_path = output / "typescript-client.json" + ready_path = output / "seller.ready.json" + if ready_path.exists(): + raise HarnessError("refusing to reuse a pre-existing Core seller readiness record") + credentials = _new_core_seller_credentials() + with stdout_path.open("wb") as seller_stdout, stderr_path.open("wb") as seller_stderr: + process = _start_ts_core_server( + node, + install, + port=port, + database_url=database_url, + credentials=credentials, + ready_file=ready_path, + cwd=output, + stdout=seller_stdout, + stderr=seller_stderr, + ) + try: + ready = _wait_core_seller_ready( + process, + ready_path, + node=node, + install=install, + credentials=credentials, + port=port, + ) + endpoint = f"http://127.0.0.1:{port}/mcp" + python_result = _run_json( + _python_core_client_command(runtime.executable, endpoint=endpoint), + cwd=output, + env=_buyer_environment(credentials.token_a), + ) + client_path.write_text( + json.dumps(python_result, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + if python_result.get("definitive") is not True: + raise HarnessError( + "Python Core reconciliation against TypeScript was not definitive" + ) + ts_result = _run_json( + _ts_core_buyer_command(node, install, endpoint=endpoint, expect_missing=False), + cwd=output, + env=_buyer_environment(credentials.token_a), + ) + ts_path.write_text( + json.dumps(ts_result, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + finally: + _stop(process) + evidence = [ + _retained(stdout_path, output), + _retained(stderr_path, output), + _retained(client_path, output), + _retained(ts_path, output), + _retained(ready_path, output), + ] + seller_process_identity = { + "pid": ready["pid"], + "start_token": ready["process_start_token"], + "startup_proof_sha256": ready["startup_proof_sha256"], + } + seller_artifact_identity = ready["seller_package"] + py_cell = { + "id": "supplemental_shared_rc4__candidate_py_client__candidate_ts_server", + "target_contract_id": "Q3", + "target_cell_id": "candidate_py_client__candidate_ts_server", + "control_classification": "historical_rc4_shared_seller_control", + "required_cell_credit": False, + "execution_attempted": True, + "observed_protocol": pin["protocol"], + "shared_seller_database": True, + "status": "partial_pass", + "cell_complete": False, + "acceptance": False, + "reason": "Core control passed; Managed Delivery and the full scenario set remain open", + "database": database_identity, + "seller_process_identity": seller_process_identity, + "seller_artifact_identity": seller_artifact_identity, + "python_runtime": _runtime_identity(runtime, artifact), + "typescript": { + "role": install.role, + "package_lock": str(install.lock), + "package_lock_sha256": _sha256(install.lock), + "pin": pin, + }, + "result": python_result, + "evidence": evidence, + } + ts_cell = { + "id": ( + "supplemental_shared_rc4__candidate_ts_client__candidate_ts_server" + if install.role == "candidate" + else "supplemental_lead_shared__candidate_ts_client__candidate_ts_server" + ), + "target_contract_id": "Q4", + "target_cell_id": "candidate_ts_client__candidate_ts_server", + "control_classification": ( + "historical_rc4_shared_seller_control" + if install.role == "candidate" + else "supplemental_lead_shared_seller_control" + ), + "required_cell_credit": False, + "execution_attempted": True, + "observed_protocol": pin.get("protocol"), + "shared_seller_database": True, + "status": "partial_pass" if install.role == "candidate" else "supplemental_pass", + "cell_complete": False, + "acceptance": False, + "reason": ( + "Candidate Core HTTP and native Core reconciliation passed; full scenarios remain open" + if install.role == "candidate" + else "rc.44 supplemental Core lane only; it is not the blocking pin" + ), + "seller_process_identity": seller_process_identity, + "seller_artifact_identity": seller_artifact_identity, + "result": ts_result, + "evidence": evidence, + } + return [py_cell, ts_cell] + + +def _run_storyboard_server( + node: Path, + install: TypeScriptInstall, + archive: TypeScriptArchive, + *, + admin_url: str, + output: Path, + inventory: dict[str, Any], +) -> dict[str, Any]: + inventory_path = output / "inventory-input.json" + inventory_path.write_text( + json.dumps(inventory, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + orchestration_output = output / "orchestration" + stdout_path = output / "orchestration.stdout.log" + stderr_path = output / "orchestration.stderr.log" + completed = subprocess.run( + [ + sys.executable, + "-I", + str(STORYBOARD_ORCHESTRATION), + "--node-runtime", + str(node), + "--node-sha256", + _sha256(node), + "--typescript-install", + str(install.root), + "--typescript-tarball", + str(archive.path), + "--typescript-role", + install.role, + "--inventory-json", + str(inventory_path), + "--external-pg-admin-dsn", + admin_url, + "--output", + str(orchestration_output), + "--execute", + ], + cwd=ROOT, + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + timeout=1_500, + ) + stdout_path.write_text(completed.stdout, encoding="utf-8") + stderr_path.write_text(completed.stderr, encoding="utf-8") + result_path = orchestration_output / "results.json" + if completed.returncode == 2 or not result_path.is_file(): + raise HarnessError( + f"storyboard orchestration failed before producing a result: " + f"exit={completed.returncode} stderr={completed.stderr[:500]}" + ) + result = json.loads(result_path.read_text(encoding="utf-8")) + if result.get("acceptance") is not False or result.get("blocking_acceptance") is not False: + raise HarnessError("storyboard-only orchestration claimed aggregate acceptance") + return { + **result, + "role": install.role, + "evidence": [ + _retained(inventory_path, output), + _retained(stdout_path, output), + _retained(stderr_path, output), + _retained(result_path, output), + ], + } + + +def _run_webhook_receiver_refresh( + runtimes: list[PythonRuntime], + artifacts: dict[str, PythonArtifact], + *, + output: Path, +) -> dict[str, Any]: + results: list[dict[str, Any]] = [] + for runtime in runtimes: + route_root = output / f"webhook-receiver-{runtime.route}" + route_root.mkdir() + receiver_port = _free_port() + issuer_port = _free_port() + vector_dir = _python_webhook_vector_dir(runtime) + stdout_path = route_root / "receiver.stdout.log" + stderr_path = route_root / "receiver.stderr.log" + result_path = route_root / "result.json" + with stdout_path.open("wb") as receiver_stdout, stderr_path.open("wb") as receiver_stderr: + process = subprocess.Popen( + [ + str(runtime.executable), + "-I", + str(WEBHOOK_RECEIVER_SERVER), + "--receiver-port", + str(receiver_port), + "--issuer-port", + str(issuer_port), + "--vector-dir", + str(vector_dir), + ], + cwd=route_root, + stdin=subprocess.DEVNULL, + stdout=receiver_stdout, + stderr=receiver_stderr, + start_new_session=True, + ) + try: + _wait_port(process, receiver_port) + completed = subprocess.run( + [ + str(runtime.executable), + "-I", + str(WEBHOOK_RECEIVER_CLIENT), + "--receiver-url", + f"http://127.0.0.1:{receiver_port}/webhook", + "--vector-dir", + str(vector_dir), + ], + cwd=route_root, + env=os.environ.copy(), + text=True, + capture_output=True, + timeout=60, + ) + finally: + _stop(process) + try: + result = json.loads(completed.stdout) + except json.JSONDecodeError as error: + raise HarnessError( + f"webhook receiver probe emitted invalid output: {completed.stdout[:500]}" + ) from error + result_path.write_text( + json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + normative_match = result.get("normative_019_match") is True + receiver_http_mapping_match = ( + result.get("vector_019", {}).get("receiver_http_mapping_match") is True + ) + blocking_acceptance = normative_match and receiver_http_mapping_match + if result.get("blocking_acceptance") is not blocking_acceptance: + raise HarnessError( + f"webhook receiver result contract is inconsistent for {runtime.route}" + ) + expected_exit = 0 if blocking_acceptance else 1 + expected_status = "passed" if blocking_acceptance else "blocking_red" + if ( + completed.returncode != expected_exit + or result.get("status") != expected_status + or result.get("execution_completed") is not True + or result.get("historical_reproducer_mode") is not False + or len(result.get("rows", [])) != 3 + ): + raise HarnessError(f"webhook receiver refresh control failed for {runtime.route}") + results.append( + { + "route": runtime.route, + "runtime": _runtime_identity(runtime, artifacts[runtime.route]), + "result": result, + "evidence": [ + _retained(stdout_path, route_root), + _retained(stderr_path, route_root), + _retained(result_path, route_root), + ], + } + ) + classifications = {item["result"].get("stale_failure_classification") for item in results} + if len(classifications) != 1: + raise HarnessError("webhook receiver artifact routes disagree on stale classification") + normative = all(item["result"].get("normative_019_match") is True for item in results) + receiver_http_mapping = all( + item["result"].get("vector_019", {}).get("receiver_http_mapping_match") is True + for item in results + ) + blocking_acceptance = normative and receiver_http_mapping + return { + "id": "python_webhook_receiver_live_revocation", + "status": "passed" if blocking_acceptance else "blocking_red", + "blocking_acceptance": blocking_acceptance, + "normative_019_match": normative, + "receiver_http_mapping_match": receiver_http_mapping, + "stale_failure_classification": classifications.pop(), + "receiver_contract": { + "actual_socket_callback": True, + "live_revocation_fetch": True, + "stale_outage_fail_closed": True, + "refresh_recovery": True, + "raw_body_verification": True, + "replay_persistence_exercised": False, + }, + "inputs": { + "server": _retained(WEBHOOK_RECEIVER_SERVER, ROOT), + "client": _retained(WEBHOOK_RECEIVER_CLIENT, ROOT), + }, + "routes": results, + "limitations": [ + "deterministic_public_test_key", + "loopback_http_test_transport", + "does_not_close_cross_language_signer_interop", + "does_not_cover_reporting_delivery_retry_or_account_activity", + ], + } + + +def _run_candidate_python_previous_ts( + runtime: PythonRuntime, + artifact: PythonArtifact, + *, + node: Path, + install: TypeScriptInstall, + admin_url: str, + output: Path, + database: str, +) -> dict[str, Any]: + if install.role != "previous": + raise HarnessError("previous-TypeScript skew requires the previous install role") + database_identity = _create_database(admin_url, database) + port = _free_port() + stdout_path = output / "seller.stdout.log" + stderr_path = output / "seller.stderr.log" + result_path = output / "python-client.json" + ready_path = output / "seller.ready.json" + if ready_path.exists(): + raise HarnessError("refusing to reuse a pre-existing Core seller readiness record") + credentials = _new_core_seller_credentials() + with stdout_path.open("wb") as seller_stdout, stderr_path.open("wb") as seller_stderr: + process = _start_ts_core_server( + node, + install, + port=port, + database_url=_node_database_url(admin_url, database), + credentials=credentials, + ready_file=ready_path, + cwd=output, + stdout=seller_stdout, + stderr=seller_stderr, + ) + try: + ready = _wait_core_seller_ready( + process, + ready_path, + node=node, + install=install, + credentials=credentials, + port=port, + ) + result = _run_json( + _python_core_client_command( + runtime.executable, + endpoint=f"http://127.0.0.1:{port}/mcp", + ), + cwd=output, + env=_buyer_environment(credentials.token_a), + ) + finally: + _stop(process) + result_path.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + if result.get("definitive") is not True or result.get("submitted_receipts") != 0: + raise HarnessError("candidate Python did not reconcile the previous TS Core seller") + return { + "id": "supplemental_rc4_skew__candidate_py_client__previous_ts_server", + "target_contract_id": "S2", + "target_cell_id": "candidate_py_client__previous_ts_server", + "control_classification": "historical_rc4_positive_core_only", + "required_cell_credit": False, + "execution_attempted": True, + "observed_protocol": _typescript_pin(install)["protocol"], + "polarity": "positive_core_control", + "status": "partial_pass", + "cell_complete": False, + "acceptance": False, + "reason": "Core skew passed; the complete declared scenario set remains required", + "scenario_results": { + "core_reporting": "passed", + "actionable_unsupported": "not_triggered_for_supported_core_contract", + }, + "database": database_identity, + "seller_process_identity": { + "pid": ready["pid"], + "start_token": ready["process_start_token"], + "startup_proof_sha256": ready["startup_proof_sha256"], + }, + "seller_artifact_identity": ready["seller_package"], + "python_runtime": _runtime_identity(runtime, artifact), + "typescript": { + "pin": _typescript_pin(install), + "package_lock": str(install.lock), + "package_lock_sha256": _sha256(install.lock), + }, + "result": result, + "evidence": [ + _retained(stdout_path, output), + _retained(stderr_path, output), + _retained(result_path, output), + _retained(ready_path, output), + ], + } + + +def _run_previous_python_candidate_ts( + previous: PreviousPythonInput, + *, + node: Path, + install: TypeScriptInstall, + admin_url: str, + output: Path, + database: str, +) -> dict[str, Any]: + if install.role != "candidate": + raise HarnessError("previous-Python skew requires the candidate TS install role") + pins = json.loads(PINS.read_text(encoding="utf-8"))["python"]["previous_released"] + if _sha256(previous.artifact) != pins["wheel_sha256"]: + raise HarnessError("previous Python wheel bytes do not match the immutable released pin") + runtime = PythonRuntime("previous", previous.executable) + artifact = PythonArtifact("previous", previous.artifact) + database_identity = _create_database(admin_url, database) + port = _free_port() + stdout_path = output / "seller.stdout.log" + stderr_path = output / "seller.stderr.log" + result_path = output / "python-client.json" + ready_path = output / "seller.ready.json" + if ready_path.exists(): + raise HarnessError("refusing to reuse a pre-existing Core seller readiness record") + credentials = _new_core_seller_credentials() + with stdout_path.open("wb") as seller_stdout, stderr_path.open("wb") as seller_stderr: + process = _start_ts_core_server( + node, + install, + port=port, + database_url=_node_database_url(admin_url, database), + credentials=credentials, + ready_file=ready_path, + cwd=output, + stdout=seller_stdout, + stderr=seller_stderr, + ) + try: + ready = _wait_core_seller_ready( + process, + ready_path, + node=node, + install=install, + credentials=credentials, + port=port, + ) + result = _run_json( + _python_core_client_command( + previous.executable, + endpoint=f"http://127.0.0.1:{port}/mcp", + expect_unsupported=True, + ), + cwd=output, + env=_buyer_environment(credentials.token_a), + ) + finally: + _stop(process) + result_path.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + if ( + result.get("status") != "actionable_unsupported" + or result.get("mutation_attempted") is not False + or result.get("phase") != "client_construction" + ): + raise HarnessError("previous Python skew did not fail closed before transport mutation") + return { + "id": "negative_skew_refusal__previous_py_client__candidate_ts_server", + "target_contract_id": "S1", + "target_cell_id": "previous_py_client__candidate_ts_server", + "control_classification": "negative_refusal_only", + "required_cell_credit": False, + "execution_attempted": True, + "observed_protocol": _typescript_pin(install)["protocol"], + "polarity": "negative_refusal", + "status": "actionable_unsupported", + "cell_complete": False, + "scenario_complete": True, + "acceptance": False, + "reason": ( + "released Python rc.3 rejects explicit rc.4 before transport; remaining applicable " + "cell scenarios still require execution" + ), + "database": database_identity, + "seller_process_identity": { + "pid": ready["pid"], + "start_token": ready["process_start_token"], + "startup_proof_sha256": ready["startup_proof_sha256"], + }, + "seller_artifact_identity": ready["seller_package"], + "python_runtime": _runtime_identity(runtime, artifact), + "typescript": { + "pin": _typescript_pin(install), + "package_lock": str(install.lock), + "package_lock_sha256": _sha256(install.lock), + }, + "result": result, + "evidence": [ + _retained(stdout_path, output), + _retained(stderr_path, output), + _retained(result_path, output), + _retained(ready_path, output), + ], + } + + +def _execution_credit_errors(contract: dict[str, Any], observed: dict[str, Any]) -> list[str]: + """Explain why one observed control cannot count as a declared cell execution.""" + + errors: list[str] = [] + if observed.get("target_cell_id", observed.get("id")) != contract.get("id"): + errors.append("cell_id_mismatch") + if observed.get("target_contract_id") != contract.get("contract_id"): + errors.append("contract_id_mismatch") + if observed.get("execution_attempted") is not True: + errors.append("execution_not_attempted") + if observed.get("required_cell_credit") is not True: + errors.append("observed_control_disclaims_required_credit") + seller_identity = observed.get("seller_process_identity") + if not isinstance(seller_identity, dict) or not { + "pid", + "start_token", + "startup_proof_sha256", + }.issubset(seller_identity): + errors.append("seller_process_identity_missing") + seller_artifact = observed.get("seller_artifact_identity") + seller_contract = contract.get("server") + if not isinstance(seller_artifact, dict): + errors.append("seller_artifact_identity_missing") + elif isinstance(seller_contract, dict): + if seller_artifact.get("language") != seller_contract.get("language"): + errors.append("seller_language_does_not_match_cell_server") + if seller_artifact.get("package_role") != seller_contract.get("package_role"): + errors.append("seller_package_role_does_not_match_cell_server") + if not isinstance(seller_artifact.get("runtime_identity_sha256"), str): + errors.append("seller_runtime_identity_missing") + if not isinstance(seller_artifact.get("artifact_identity_sha256"), str): + errors.append("seller_archive_identity_missing") + expected_seller_archive = seller_contract.get("artifact_identity_sha256") + if expected_seller_archive is not None and ( + seller_artifact.get("artifact_identity_sha256") != expected_seller_archive + ): + errors.append("seller_archive_identity_does_not_match_cell_server") + expected_seller_runtime = seller_contract.get("runtime_identity_sha256") + if expected_seller_runtime is not None and ( + seller_artifact.get("runtime_identity_sha256") != expected_seller_runtime + ): + errors.append("seller_runtime_identity_does_not_match_cell_server") + database_identity = observed.get("database_identity") + if not isinstance(database_identity, dict) or not { + "name", + "cluster_identity", + }.issubset(database_identity): + errors.append("database_identity_missing") + + protocol = contract.get("protocol_contract") + if protocol is not None: + if protocol.get("selected_artifacts") is not True: + errors.append("contract_artifacts_not_selected") + expected_artifacts = protocol.get("artifact_identity_sha256") + if not isinstance(expected_artifacts, str) or len(expected_artifacts) != 64: + errors.append("contract_artifact_identity_missing") + elif observed.get("artifact_identity_sha256") != expected_artifacts: + errors.append("observed_artifact_identity_does_not_match_required") + if protocol.get("alignment") != "exact": + errors.append("contract_protocol_alignment_not_exact") + if observed.get("observed_protocol") != protocol.get("required"): + errors.append("observed_protocol_does_not_match_required") + if observed.get("polarity") != "positive_semantic": + errors.append("positive_semantic_polarity_not_proven") + + positive = contract.get("positive_requirement") + if positive is not None: + if positive.get("status") != "selected_supported_pair": + errors.append("positive_skew_pair_not_selected") + expected_artifacts = positive.get("artifact_identity_sha256") + if not isinstance(expected_artifacts, str) or len(expected_artifacts) != 64: + errors.append("positive_skew_artifact_identity_missing") + elif observed.get("artifact_identity_sha256") != expected_artifacts: + errors.append("observed_artifact_identity_does_not_match_positive_skew_pair") + if observed.get("polarity") != "positive_supported_skew": + errors.append("negative_or_partial_skew_cannot_satisfy_positive_requirement") + + if contract.get("family") == "latest_canary": + if observed.get("control_classification") != "visible_latest_canary": + errors.append("visible_canary_classification_missing") + expected_artifacts = contract.get("artifact_identity_sha256") + if not isinstance(expected_artifacts, str) or len(expected_artifacts) != 64: + errors.append("visible_canary_artifact_identity_missing") + elif observed.get("artifact_identity_sha256") != expected_artifacts: + errors.append("visible_canary_artifact_identity_mismatch") + return errors + + +def _execution_accounting( + applicability: dict[str, Any], cells: list[dict[str, Any]] +) -> list[dict[str, Any]]: + """Account for required and visible cells without trusting a returned ID alone.""" + + prepared: list[tuple[dict[str, Any], list[tuple[dict[str, Any], dict[str, Any]]]]] = [] + for contract in applicability["cells"]: + observed = [ + cell for cell in cells if cell.get("target_cell_id", cell.get("id")) == contract["id"] + ] + evaluations = [ + {"observed_id": cell.get("id"), "errors": _execution_credit_errors(contract, cell)} + for cell in observed + ] + prepared.append((contract, list(zip(observed, evaluations, strict=True)))) + + potentially_credited = [ + cell + for _contract, pairs in prepared + for cell, evaluation in pairs + if not evaluation["errors"] + ] + + def identity_key(value: dict[str, Any]) -> str: + return json.dumps(value, sort_keys=True, separators=(",", ":")) + + seller_counts = Counter( + identity_key(cell["seller_process_identity"]) for cell in potentially_credited + ) + database_counts = Counter( + identity_key(cell["database_identity"]) for cell in potentially_credited + ) + for _contract, pairs in prepared: + for cell, evaluation in pairs: + if evaluation["errors"]: + continue + if seller_counts[identity_key(cell["seller_process_identity"])] != 1: + evaluation["errors"].append("seller_process_identity_not_unique") + if database_counts[identity_key(cell["database_identity"])] != 1: + evaluation["errors"].append("database_identity_not_unique") + + rows: list[dict[str, Any]] = [] + for contract, pairs in prepared: + credited = [cell for cell, evaluation in pairs if not evaluation["errors"]] + rows.append( + { + "id": contract["id"], + "contract_id": contract.get("contract_id"), + "family": contract["family"], + "blocking": contract["blocking"], + "observed_controls": [evaluation for _cell, evaluation in pairs], + "executed_with_required_credit": bool(credited), + "completed_with_required_credit": any( + cell.get("cell_complete") is True for cell in credited + ), + } + ) + return rows + + +def main() -> None: + args = _arguments() + args.output = args.output.absolute() + routes = [runtime.route for runtime in args.python_runtime] + if len(routes) != len(set(routes)): + raise SystemExit("each Python artifact route may be supplied only once") + artifacts = {artifact.route: artifact for artifact in args.python_artifact} + if len(artifacts) != len(args.python_artifact): + raise SystemExit("each Python artifact route may be supplied only once") + if set(routes) != set(artifacts): + raise SystemExit("Python runtime and artifact routes must match exactly") + _validate_python_artifact_inputs(artifacts) + if bool(args.previous_python_runtime) != bool(args.previous_python_artifact): + raise SystemExit( + "--previous-python-runtime and --previous-python-artifact must be supplied together" + ) + previous_python = None + if args.previous_python_runtime and args.previous_python_artifact: + previous_executable = args.previous_python_runtime.absolute() + previous_artifact = args.previous_python_artifact.absolute() + if not previous_executable.is_file() or not previous_artifact.is_file(): + raise SystemExit("previous Python runtime or artifact is unavailable") + previous_python = PreviousPythonInput(previous_executable, previous_artifact) + dependency_labels = [runtime.label for runtime in args.python_dependency_runtime] + if len(dependency_labels) != len(set(dependency_labels)): + raise SystemExit("each Python dependency runtime label may be supplied only once") + installs = {install.role: install for install in args.typescript_install} + if len(installs) != len(args.typescript_install): + raise SystemExit("each TypeScript install role may be supplied only once") + archives = {archive.role: archive for archive in args.typescript_archive} + if len(archives) != len(args.typescript_archive): + raise SystemExit("each TypeScript archive role may be supplied only once") + if set(archives) != set(installs): + raise SystemExit("TypeScript install and archive roles must match exactly") + node = args.node_runtime.absolute() if args.node_runtime else None + if installs and (node is None or not node.is_file()): + raise SystemExit("--node-runtime is required with TypeScript installs") + node_identity = _node_identity(node) if node is not None else None + typescript_archive_identities = { + role: _typescript_archive_identity(install, archives[role]) + for role, install in sorted(installs.items()) + } + args.output.mkdir(parents=True, exist_ok=False) + run_id = secrets.token_hex(6) + cells: list[dict[str, Any]] = [] + storyboard_inventories: list[dict[str, Any]] = [] + storyboard_executions: list[dict[str, Any]] = [] + inventory_by_role: dict[str, dict[str, Any]] = {} + blocking_findings: list[dict[str, Any]] = [] + advisory_findings: list[dict[str, Any]] = [] + dependency_controls: list[dict[str, Any]] = [] + created: list[str] = [] + try: + for runtime in args.python_dependency_runtime: + dependency_controls.append( + _run_dependency_control( + runtime, + artifacts[runtime.artifact_route], + output=args.output, + ) + ) + if node is not None: + for role, install in sorted(installs.items()): + inventory = _run_json( + _storyboard_inventory_command(node, install), + cwd=args.output, + env=os.environ.copy(), + ) + inventory_path = args.output / f"storyboards-{role}.json" + inventory_path.write_text( + json.dumps(inventory, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + storyboard_inventories.append( + { + "role": role, + "result": inventory, + "evidence": _retained(inventory_path, args.output), + "execution_complete": False, + } + ) + inventory_by_role[role] = inventory + if "candidate" in installs: + advisory_findings.append( + _run_resource_location_comparison( + args.python_runtime, + artifacts, + node=node, + install=installs["candidate"], + output=args.output, + ) + ) + advisory_findings.extend( + [ + { + "id": "typescript_response_headers_metadata", + "status": "supplied_red_not_reexecuted_by_this_matrix", + "blocking": False, + "applicability": "visible_nonblocking", + "typescript_pin": _typescript_pin(installs["candidate"]), + "observed": ( + "responseHeaders omission affects diagnostic response headers" + ), + "boundaries": [ + "independently reproduced at Undici 6.28.0 and 6.28.1", + "not inferred green from candidate publication", + "not reporting-semantic acceptance evidence", + ], + }, + { + "id": "typescript_specialism_timeout_fragility", + "status": "supplied_advisory_not_reexecuted_by_this_matrix", + "blocking": False, + "applicability": "visible_nonblocking", + "typescript_pin": _typescript_pin(installs["candidate"]), + "boundaries": [ + "no retry or test-budget expansion applied", + "separate from response-header metadata", + ], + }, + ] + ) + blocking_findings.append( + { + "id": "typescript_candidate_official_precedence", + **_run_official_precedence(node, installs["candidate"], args.output), + } + ) + blocking_findings.append( + _run_webhook_receiver_refresh( + args.python_runtime, + artifacts, + output=args.output, + ) + ) + blocking_findings.append( + { + "id": "protocol_rc4_webhook_signing_conformance", + **_run_protocol_webhook_vectors( + args.python_runtime, + artifacts, + node=node, + install=installs["candidate"], + output=args.output, + ), + } + ) + blocking_findings.append( + { + "id": "python_typescript_webhook_signer_interop", + **_run_cross_language_webhook_signing( + args.python_runtime, + artifacts, + node=node, + install=installs["candidate"], + output=args.output, + ), + } + ) + signing_database = f"adcp_i_{run_id}_signing" + created.append(signing_database) + blocking_findings.append( + { + "id": "typescript_candidate_signing_and_hmac_controls", + **_run_signing_vectors( + node, + installs["candidate"], + admin_url=args.pg_admin_url, + output=args.output, + database=signing_database, + ), + } + ) + for index, runtime in enumerate(args.python_runtime): + cell_root = args.output / runtime.route / "candidate_py_client__candidate_py_server" + cell_root.mkdir(parents=True) + database = f"adcp_i_{run_id}_{index}" + created.append(database) + cells.extend( + _run_python_control( + runtime, + artifacts[runtime.route], + admin_url=args.pg_admin_url, + output=cell_root, + database=database, + node=node, + candidate_ts=installs.get("candidate"), + ) + ) + candidate_ts = installs.get("candidate") + if candidate_ts is not None and node is not None: + ts_root = args.output / runtime.route / "candidate_ts_server_core" + ts_root.mkdir(parents=True) + ts_database = f"adcp_i_{run_id}_ts_{index}" + created.append(ts_database) + cells.extend( + _run_ts_core_control( + runtime, + artifacts[runtime.route], + node=node, + install=candidate_ts, + admin_url=args.pg_admin_url, + output=ts_root, + database=ts_database, + ) + ) + lead = installs.get("lead") + if lead is not None and node is not None: + runtime = args.python_runtime[0] + lead_root = args.output / "supplemental" / "rc44_core" + lead_root.mkdir(parents=True) + lead_database = f"adcp_i_{run_id}_lead" + created.append(lead_database) + lead_cells = _run_ts_core_control( + runtime, + artifacts[runtime.route], + node=node, + install=lead, + admin_url=args.pg_admin_url, + output=lead_root, + database=lead_database, + ) + cells.extend(lead_cells) + previous_ts = installs.get("previous") + if previous_ts is not None and node is not None: + for index, runtime in enumerate(args.python_runtime): + skew_root = args.output / runtime.route / "candidate_py_client__previous_ts_server" + skew_root.mkdir(parents=True) + skew_database = f"adcp_i_{run_id}_skew_ts_{index}" + created.append(skew_database) + cells.append( + _run_candidate_python_previous_ts( + runtime, + artifacts[runtime.route], + node=node, + install=previous_ts, + admin_url=args.pg_admin_url, + output=skew_root, + database=skew_database, + ) + ) + candidate_ts = installs.get("candidate") + if previous_python is not None and candidate_ts is not None and node is not None: + skew_root = args.output / "previous" / "previous_py_client__candidate_ts_server" + skew_root.mkdir(parents=True) + skew_database = f"adcp_i_{run_id}_skew_py" + created.append(skew_database) + cells.append( + _run_previous_python_candidate_ts( + previous_python, + node=node, + install=candidate_ts, + admin_url=args.pg_admin_url, + output=skew_root, + database=skew_database, + ) + ) + if node is not None: + for role, install in sorted(installs.items()): + storyboard_root = args.output / "storyboards" / role + storyboard_root.mkdir(parents=True) + storyboard_executions.append( + _run_storyboard_server( + node, + install, + archives[role], + admin_url=args.pg_admin_url, + output=storyboard_root, + inventory=inventory_by_role[role], + ) + ) + finally: + if not args.keep_databases: + for database in reversed(created): + _drop_database(args.pg_admin_url, database) + + applicability = json.loads(APPLICABILITY.read_text(encoding="utf-8")) + execution_accounting = _execution_accounting(applicability, cells) + required_rows = [row for row in execution_accounting if row["blocking"]] + visible_rows = [row for row in execution_accounting if not row["blocking"]] + required = {row["id"] for row in required_rows} + executed = {row["id"] for row in required_rows if row["executed_with_required_credit"] is True} + completed = { + row["id"] for row in required_rows if row["completed_with_required_credit"] is True + } + executed_visible = { + row["id"] for row in visible_rows if row["executed_with_required_credit"] is True + } + required_visible = {row["id"] for row in visible_rows} + report = { + "schema_version": 1, + "phase": "foundation_qualification_preflight", + "acceptance": False, + "status": "incomplete", + "run_id": run_id, + "executed_blocking_cells": sorted(executed), + "missing_blocking_cells": sorted(required - executed), + "incomplete_blocking_cells": sorted(required - completed), + "executed_visible_canaries": sorted(executed_visible), + "missing_visible_canaries": sorted(required_visible - executed_visible), + "cell_execution_accounting": execution_accounting, + "artifact_routes": routes, + "harness": { + **_git_identity(), + "inputs": { + path.name: { + "path": str(path.relative_to(ROOT)), + "sha256": _sha256(path), + } + for path in [APPLICABILITY, HERE / "corpus.json", HERE / "pins.json"] + }, + }, + "cells": cells, + "node_runtime": node_identity, + "typescript_archive_identities": typescript_archive_identities, + "storyboard_inventories": storyboard_inventories, + "storyboard_executions": storyboard_executions, + "blocking_findings": blocking_findings, + "advisory_findings": advisory_findings, + "dependency_controls": dependency_controls, + } + report_path = args.output / "results.json" + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(report, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/reporting_interop/security/resource_location_python.original.py.txt b/scripts/ci/reporting_interop/security/resource_location_python.original.py.txt new file mode 100644 index 000000000..2a7d66f5e --- /dev/null +++ b/scripts/ci/reporting_interop/security/resource_location_python.original.py.txt @@ -0,0 +1,11 @@ +import json, sys +from adcp.reporting.evidence import resource_location +corpus = json.load(open(sys.argv[1])) +out=[] +for c in corpus: + try: + resource_location(c["v"]); r="accept" + except ValueError: r="reject" + except Exception as e: r=f"error:{type(e).__name__}" + out.append({"id":c["id"],"expect":c["expect"],"python":r}) +print(json.dumps(out)) diff --git a/scripts/ci/reporting_interop/security/resource_location_sentinels.json b/scripts/ci/reporting_interop/security/resource_location_sentinels.json new file mode 100644 index 000000000..4565cdf2f --- /dev/null +++ b/scripts/ci/reporting_interop/security/resource_location_sentinels.json @@ -0,0 +1,19 @@ +[ + {"id":"plain-relation","v":"share.billing.august","expect":"accept"}, + {"id":"plain-dataset","v":"analytics_db.reporting.august_2026","expect":"accept"}, + {"id":"provider-path","v":"warehouse/prod/reporting/august","expect":"accept"}, + {"id":"https-clean","v":"https://data.example.test/reporting/august","expect":"either"}, + {"id":"presigned-aws","v":"https://s3.example.test/r.parquet?X-Amz-Signature=RLXSENTINEL0001&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE","expect":"reject"}, + {"id":"azure-sas","v":"https://acct.blob.core.windows.test/c/r.parquet?sv=2021-08-06&sig=RLXSENTINEL0002","expect":"reject"}, + {"id":"gcs-token","v":"https://storage.googleapis.test/b/o?access_token=RLXSENTINEL0003","expect":"reject"}, + {"id":"userinfo-url","v":"https://user:RLXSENTINEL0004@data.example.test/r.parquet","expect":"reject"}, + {"id":"bare-jwt","v":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJSTFhTRU5USU5FTDAwMDUifQ.c2ln","expect":"reject"}, + {"id":"aws-key-id","v":"dataset AKIAIOSFODNN7EXAMPLE august","expect":"reject"}, + {"id":"kv-assignment","v":"share.billing;authorization=RLXSENTINEL0006","expect":"reject"}, + {"id":"kv-apikey","v":"relation?api_key=RLXSENTINEL0007","expect":"reject"}, + {"id":"pct-encoded-query","v":"https://s3.example.test/r.parquet%3FX-Amz-Signature%3DRLXSENTINEL0008","expect":"reject"}, + {"id":"double-pct-encoded","v":"https://s3.example.test/r%253FX-Amz-Signature%253DRLXSENTINEL0009","expect":"reject"}, + {"id":"protocol-relative","v":"//data.example.test/r.parquet","expect":"reject"}, + {"id":"http-plain","v":"http://data.example.test/r.parquet","expect":"reject"}, + {"id":"fragment","v":"https://data.example.test/r.parquet#RLXSENTINEL0010","expect":"reject"} +] diff --git a/scripts/ci/reporting_interop/security/resource_location_typescript.cjs b/scripts/ci/reporting_interop/security/resource_location_typescript.cjs new file mode 100644 index 000000000..84b819e8d --- /dev/null +++ b/scripts/ci/reporting_interop/security/resource_location_typescript.cjs @@ -0,0 +1,13 @@ +const fs=require('fs'),path=require('path'),Module=require('node:module'); +const base=path.resolve(process.argv[2]); +const req=Module.createRequire(path.join(base,'noop.js')); +const led=req('@adcp/sdk/reporting/ledger'); +const fn=led.assertCredentialFreeReportingResourceLocationV1; +const corpus=JSON.parse(fs.readFileSync(process.argv[3],'utf8')); +const out=corpus.map(c=>{ + let r; + try{ fn(c.v); r='accept'; } + catch(e){ r='reject'; } + return {id:c.id, expect:c.expect, ts:r}; +}); +console.log(JSON.stringify({available:typeof fn, rows:out})); diff --git a/scripts/ci/reporting_interop/signing/crossverify_python.py b/scripts/ci/reporting_interop/signing/crossverify_python.py new file mode 100644 index 000000000..2a19aedfc --- /dev/null +++ b/scripts/ci/reporting_interop/signing/crossverify_python.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Verify a synthetic public-test-key webhook sample with the Python API.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +from adcp.signing.errors import SignatureVerificationError +from adcp.signing.webhook_verifier import WebhookVerifyOptions, verify_webhook_signature + + +def main() -> None: + sample = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + vector_dir = Path(sys.argv[2]) + keys = { + key["kid"]: {name: value for name, value in key.items() if not name.startswith("_")} + for key in json.loads((vector_dir / "keys.json").read_text(encoding="utf-8"))["keys"] + } + request = sample["request"] + try: + verify_webhook_signature( + method=request["method"], + url=request["url"], + headers=request["headers"], + body=request["body"].encode(), + options=WebhookVerifyOptions( + jwks_resolver=lambda kid: keys.get(kid), + clock=lambda: 1776520800.0, + ), + ) + result = {"accepted": True} + except SignatureVerificationError as error: + result = {"accepted": False, "code": error.code, "step": error.step} + print(json.dumps(result, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/reporting_interop/signing/crossverify_typescript.cjs b/scripts/ci/reporting_interop/signing/crossverify_typescript.cjs new file mode 100644 index 000000000..5ebc36b58 --- /dev/null +++ b/scripts/ci/reporting_interop/signing/crossverify_typescript.cjs @@ -0,0 +1,23 @@ +'use strict'; +const fs=require('fs'), path=require('path'), Module=require('node:module'); +const base=path.resolve(process.argv[2]); +const req=Module.createRequire(path.join(base,'noop.js')); +const S=req('@adcp/sdk/signing'); +const sample=JSON.parse(fs.readFileSync(process.argv[3],'utf8')); +const keys=JSON.parse(fs.readFileSync(path.join(process.argv[4],'keys.json'),'utf8')).keys + .map(k=>Object.fromEntries(Object.entries(k).filter(([n])=>!n.startsWith('_')))); +const byKid=Object.fromEntries(keys.map(k=>[k.kid,k])); +(async()=>{ + const opts={ + jwks:{ async resolve(kid){ return byKid[kid] ?? null; } }, + replayStore:{ async has(){return false;}, async isCapHit(){return false;}, async insert(){return 'ok';} }, + revocationStore:{ async isRevoked(){return false;} }, + now: ()=>1776520800, + }; + try{ + const r=await S.verifyWebhookSignature(sample.request, opts); + console.log(JSON.stringify({ts_accepts_python_signed_webhook:true, result:r})); + }catch(e){ + console.log(JSON.stringify({ts_accepts_python_signed_webhook:false, code:e?.code, message:(e?.message||'').slice(0,140)})); + } +})(); diff --git a/scripts/ci/reporting_interop/signing/run-signing-vectors.cjs b/scripts/ci/reporting_interop/signing/run-signing-vectors.cjs new file mode 100644 index 000000000..7572480d0 --- /dev/null +++ b/scripts/ci/reporting_interop/signing/run-signing-vectors.cjs @@ -0,0 +1,218 @@ +'use strict'; +// Drives the separate-process RFC 9421 verifier over real HTTP with deterministic +// injected clocks. Also exercises the SEPARATE legacy HMAC webhook utility so the +// two are never conflated. +const assert = require('node:assert/strict'); +const crypto = require('node:crypto'); +const { spawn } = require('node:child_process'); +const fs = require('node:fs'); +const { mintEphemeralEd25519Key } = require('@adcp/sdk/signing/testing'); +const { signRequest, computeContentDigest, requestSigningEncodingForVersion } = require('@adcp/sdk/signing/client'); +const { verifyWebhookRequest } = require('@adcp/sdk/webhooks'); +const { CLOCK_SKEW_TOLERANCE_SECONDS, MAX_SIGNATURE_WINDOW_SECONDS, ALLOWED_ALGS, + MANDATORY_COMPONENTS, WEBHOOK_MANDATORY_COMPONENTS } = require('@adcp/sdk/signing/server'); + +const PORT = Number(process.env.PROBE_PORT || 54321); +const PG_URL = process.env.PROBE_PG_URL; +const ADCP_VERSION_PIN = '3.2.0-rc.4'; +const BINARY_ENCODING = requestSigningEncodingForVersion(ADCP_VERSION_PIN); +// Every `now` injection point in this SDK is UNIX SECONDS (verifier, signer and +// webhook all default to Math.floor(Date.now()/1e3)). Passing milliseconds makes +// PostgresReplayStore fail closed on its year-9999 upper bound. +const T0_SEC = Math.floor(Date.parse('2026-09-22T12:00:00Z') / 1000); +const BODY = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', + params: { name: 'get_reporting_status', arguments: { account: { account_id: 'account-1' } } } }); + +let child; +const children = []; +function boot(jwks) { + return new Promise((resolve, reject) => { + const c = spawn(process.execPath, ['verifier-server.cjs'], { + cwd: __dirname, + env: { ...process.env, PROBE_PORT: String(PORT), PROBE_PG_URL: PG_URL, PROBE_JWKS: JSON.stringify(jwks) }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + children.push(c); + let buf = ''; + c.stdout.on('data', d => { buf += d; if (buf.includes('LISTENING')) resolve(c); }); + c.stderr.on('data', d => process.stderr.write('[server] ' + d)); + c.on('exit', code => { if (!buf.includes('LISTENING')) reject(new Error('server exited ' + code)); }); + setTimeout(() => reject(new Error('server boot timeout')), 15000); + }); +} +async function reap() { + for (const c of children) { try { c.kill('SIGTERM'); } catch {} } + await new Promise(r => setTimeout(r, 300)); + for (const c of children) { try { if (c.exitCode === null) c.kill('SIGKILL'); } catch {} } +} + +function signed(key, { nowSec = T0_SEC, nonce, body = BODY, path = '/mcp', method = 'POST', coverContentDigest = true } = {}) { + const base = { + method, url: `http://127.0.0.1:${PORT}${path}`, + headers: { 'content-type': 'application/json' }, body, + }; + return signRequest(base, key, { coverContentDigest, now: () => nowSec, binaryEncoding: BINARY_ENCODING, ...(nonce ? { nonce } : {}) }); +} + +async function post(headers, body, { nowSec = T0_SEC, operation } = {}) { + const res = await fetch(`http://127.0.0.1:${PORT}/mcp`, { + method: 'POST', + headers: { ...headers, 'x-probe-now-sec': String(nowSec), ...(operation ? { 'x-probe-operation': operation } : {}) }, + body, + }); + return res.json(); +} + +const rows = []; +function record(id, expectation, observed, pass, extra = {}) { + rows.push({ vectorId: id, expected: expectation, observed, pass, ...extra }); + console.error(` [${pass ? 'PASS' : 'FAIL'}] ${id} expected=${expectation} observed=${observed}`); +} + +async function main() { + const reqKey = await mintEphemeralEd25519Key({ kid: 'probe-request-key-1', adcp_use: 'request-signing' }); + const otherKey = await mintEphemeralEd25519Key({ kid: 'probe-unknown-key', adcp_use: 'request-signing' }); + const signerKey = { keyid: reqKey.kid, alg: reqKey.algorithm, privateKey: reqKey.privateKey }; + child = await boot([reqKey.publicKey]); // only reqKey is published + + // V1 valid signed request + { + const s = signed(signerKey, { nonce: 'nonce-v1' }); + const r = await post(s.headers, s.body ?? BODY, { operation: 'get_reporting_status' }); + const st = r.ok ? r.result.status : 'threw:' + r.error.message; + record('sig/valid_signed_request', 'verified', st, st === 'verified', + { keyid: r.ok ? r.result.keyid : undefined, rawBodySha256: r.rawBodySha256 }); + } + // V2 exact replay of the same nonce -> must be rejected by the PG replay store + { + const s = signed(signerKey, { nonce: 'nonce-replay' }); + const first = await post(s.headers, s.body ?? BODY, { operation: 'get_reporting_status' }); + const second = await post(s.headers, s.body ?? BODY, { operation: 'get_reporting_status' }); + const f = first.ok ? first.result.status : 'threw'; + const sec = second.ok ? second.result.status : 'threw'; + record('sig/replay_same_nonce_rejected', 'first=verified,second!=verified', + `first=${f},second=${sec}`, f === 'verified' && sec !== 'verified', + { secondDetail: second.ok ? second.result : second.error }); + } + // V3 body tampered after signing -> content-digest / signature must fail + { + const s = signed(signerKey, { nonce: 'nonce-tamper' }); + const tampered = BODY.replace('account-1', 'account-2'); + assert.notEqual(tampered, BODY); + const r = await post(s.headers, tampered, { operation: 'get_reporting_status' }); + const st = r.ok ? r.result.status : 'threw'; + record('sig/tampered_body_rejected', '!verified', st, st !== 'verified', + { detail: r.ok ? r.result : r.error }); + } + // V4 signature older than MAX_SIGNATURE_WINDOW_SECONDS (injected verifier clock) + { + const s = signed(signerKey, { nonce: 'nonce-expired', nowSec: T0_SEC }); + const late = T0_SEC + MAX_SIGNATURE_WINDOW_SECONDS + 100; + const r = await post(s.headers, s.body ?? BODY, { nowSec: late, operation: 'get_reporting_status' }); + const st = r.ok ? r.result.status : 'threw'; + record('sig/expired_beyond_max_window_rejected', '!verified', st, st !== 'verified', + { maxWindowSeconds: MAX_SIGNATURE_WINDOW_SECONDS, detail: r.ok ? r.result : r.error }); + } + // V5 signature created in the future beyond CLOCK_SKEW_TOLERANCE_SECONDS + { + const future = T0_SEC + CLOCK_SKEW_TOLERANCE_SECONDS + 120; + const s = signed(signerKey, { nonce: 'nonce-future', nowSec: future }); + const r = await post(s.headers, s.body ?? BODY, { nowSec: T0_SEC, operation: 'get_reporting_status' }); + const st = r.ok ? r.result.status : 'threw'; + record('sig/future_beyond_skew_rejected', '!verified', st, st !== 'verified', + { skewToleranceSeconds: CLOCK_SKEW_TOLERANCE_SECONDS, detail: r.ok ? r.result : r.error }); + } + // V6 unknown keyid (key never published in the JWKS) + { + const s = signed({ keyid: otherKey.kid, alg: otherKey.algorithm, privateKey: otherKey.privateKey }, + { nonce: 'nonce-unknown-key' }); + const r = await post(s.headers, s.body ?? BODY, { operation: 'get_reporting_status' }); + const st = r.ok ? r.result.status : 'threw'; + record('sig/unknown_keyid_rejected', '!verified', st, st !== 'verified', + { detail: r.ok ? r.result : r.error }); + } + // V7 unsigned request for an operation in capability.required_for + { + const r = await post({ 'content-type': 'application/json' }, BODY, { operation: 'get_reporting_status' }); + const st = r.ok ? r.result.status : 'threw'; + record('sig/unsigned_required_operation_rejected', '!verified', st, st !== 'verified', + { detail: r.ok ? r.result : r.error }); + } + // V8 unsigned request for an operation NOT in required_for -> unsigned, not an error + { + const r = await post({ 'content-type': 'application/json' }, BODY, { operation: 'list_creative_formats' }); + const st = r.ok ? r.result.status : 'threw'; + record('sig/unsigned_unrequired_operation_not_verified_but_not_error', 'unsigned-ish (!verified, no throw)', + st, r.ok === true && st !== 'verified', { detail: r.ok ? r.result : r.error }); + } + // V9 concurrent duplicate nonce race -> exactly one verified + { + const s = signed(signerKey, { nonce: 'nonce-race' }); + const results = await Promise.all(Array.from({ length: 8 }, () => + post(s.headers, s.body ?? BODY, { operation: 'get_reporting_status' }))); + const verified = results.filter(r => r.ok && r.result.status === 'verified').length; + record('sig/concurrent_duplicate_nonce_exactly_one', 'verifiedCount=1', + `verifiedCount=${verified}`, verified === 1, { attempts: results.length }); + } + // V10 PG replay durability across verifier process restart (same durable DB) + { + const s = signed(signerKey, { nonce: 'nonce-durable' }); + const first = await post(s.headers, s.body ?? BODY, { operation: 'get_reporting_status' }); + await reap(); + child = await boot([reqKey.publicKey]); // fresh process, same PostgreSQL + const afterRestart = await post(s.headers, s.body ?? BODY, { operation: 'get_reporting_status' }); + const f = first.ok ? first.result.status : 'threw'; + const a = afterRestart.ok ? afterRestart.result.status : 'threw'; + record('sig/replay_survives_verifier_restart', 'first=verified,afterRestart!=verified', + `first=${f},afterRestart=${a}`, f === 'verified' && a !== 'verified', + { note: 'same durable PostgreSQL replay cache, new verifier process' }); + } + + // W1..W3 the SEPARATE legacy HMAC webhook utility - distinct from RFC 9421 + { + const secret = 'probe-shared-secret'; + const ts = T0_SEC; + const payload = JSON.stringify({ notification_type: 'reporting_ledger_changed' }); + const mac = crypto.createHmac('sha256', secret).update(String(ts)).update('.').update(Buffer.from(payload)).digest('hex'); + const good = verifyWebhookRequest({ secret, rawBody: payload, + headers: { 'x-adcp-signature': `sha256=${mac}`, 'x-adcp-timestamp': String(ts) }, now: () => T0_SEC }); + record('hmac_webhook/valid_accepted', 'ok=true', `ok=${good.ok}`, good.ok === true, { detail: good }); + + const badMac = verifyWebhookRequest({ secret, rawBody: payload, + headers: { 'x-adcp-signature': `sha256=${'0'.repeat(64)}`, 'x-adcp-timestamp': String(ts) }, now: () => T0_SEC }); + record('hmac_webhook/wrong_mac_rejected', 'ok=false', `ok=${badMac.ok}`, badMac.ok === false, + { reason: badMac.reason }); + + const tampered = verifyWebhookRequest({ secret, rawBody: payload.replace('changed', 'ready'), + headers: { 'x-adcp-signature': `sha256=${mac}`, 'x-adcp-timestamp': String(ts) }, now: () => T0_SEC }); + record('hmac_webhook/tampered_body_rejected', 'ok=false', `ok=${tampered.ok}`, tampered.ok === false, + { reason: tampered.reason }); + } + + const report = { + scope: 'real_separate_process_http_rfc9421_verifier_with_real_postgresql_replay_store', + pin: { package: '@adcp/sdk', version: require('@adcp/sdk/package.json').version, + adcpVersion: require('@adcp/sdk').ADCP_VERSION }, + runtime: { node: process.version, platform: process.platform, arch: process.arch }, + constants: { + CLOCK_SKEW_TOLERANCE_SECONDS, MAX_SIGNATURE_WINDOW_SECONDS, + ALLOWED_ALGS: Array.from(ALLOWED_ALGS || []), + MANDATORY_COMPONENTS, WEBHOOK_MANDATORY_COMPONENTS, + }, + distinction: { + rfc9421: '@adcp/sdk/signing/server verifyRequestSignature - asymmetric JWKS, Signature-Input, replay store, content-digest', + legacyHmac: '@adcp/sdk/webhooks verifyWebhookRequest - shared-secret HMAC-SHA256 over timestamp + "." + rawBody, header x-adcp-signature: sha256=', + }, + deterministicClockBaseUnixSeconds: T0_SEC, + binaryEncoding: BINARY_ENCODING, + adcpVersionPin: ADCP_VERSION_PIN, + totals: { vectors: rows.length, passed: rows.filter(r => r.pass).length, failed: rows.filter(r => !r.pass).length }, + vectors: rows, + }; + fs.writeFileSync(process.argv[2], JSON.stringify(report, null, 2) + '\n'); + console.error(JSON.stringify(report.totals)); + process.exitCode = report.totals.failed === 0 ? 0 : 1; +} + +main().catch(e => { console.error('HARNESS FAILURE', e); process.exitCode = 2; }) + .finally(async () => { await reap(); }); diff --git a/scripts/ci/reporting_interop/signing/sign_webhook_typescript.cjs b/scripts/ci/reporting_interop/signing/sign_webhook_typescript.cjs new file mode 100644 index 000000000..b3d4e2777 --- /dev/null +++ b/scripts/ci/reporting_interop/signing/sign_webhook_typescript.cjs @@ -0,0 +1,18 @@ +'use strict'; +const fs=require('fs'), path=require('path'), Module=require('node:module'); +const req=Module.createRequire(path.join(path.resolve(process.argv[2]),'noop.js')); +const S=req('@adcp/sdk/signing'); +const V=process.argv[3]; +const k=JSON.parse(fs.readFileSync(path.join(V,'keys.json'),'utf8')).keys + .find(x=>x.kid==='test-ed25519-webhook-2026'); +const body='{"idempotency_key":"whk_RLX0000000000000000000002","task_id":"t1","operation_id":"op1","status":"completed"}'; +const url='https://buyer.example.com/adcp/webhook/create_media_buy/agent_123/op_abc'; +const privateKey={kty:k.kty,crv:k.crv,x:k.x,d:k._private_d_for_test_only,alg:k.alg,kid:k.kid, + use:'sig',key_ops:['sign'],adcp_use:k.adcp_use}; +const out=S.signWebhook( + {method:'POST',url,headers:{'Content-Type':'application/json'},body}, + {keyid:k.kid, alg:'ed25519', privateKey}, + {nonce:'RLXnonceTS0000000000', now:()=>1776520800, windowSeconds:300}); +const hdrs=out.headers||out; +console.log(JSON.stringify({emitted:hdrs, request:{method:'POST',url, + headers:{'Content-Type':'application/json',...hdrs}, body}},null,1)); diff --git a/scripts/ci/reporting_interop/signing/signer_roundtrip_python.original.py.txt b/scripts/ci/reporting_interop/signing/signer_roundtrip_python.original.py.txt new file mode 100644 index 000000000..79c41ccc3 --- /dev/null +++ b/scripts/ci/reporting_interop/signing/signer_roundtrip_python.original.py.txt @@ -0,0 +1,37 @@ +"""RLX-XL-003: what alphabet does Python's own sign_webhook emit, and does TS accept it?""" +import base64, json, re, sys +from pathlib import Path +from cryptography.hazmat.primitives.asymmetric import ed25519 +from adcp.signing.webhook_signer import sign_webhook + +VEC = Path(sys.argv[1]) +ks = {k["kid"]: k for k in json.loads((VEC / "keys.json").read_text())["keys"]} +k = ks["test-ed25519-webhook-2026"] +d = base64.urlsafe_b64decode(k["_private_d_for_test_only"] + "=" * (-len(k["_private_d_for_test_only"]) % 4)) +priv = ed25519.Ed25519PrivateKey.from_private_bytes(d) + +body = b'{"idempotency_key":"whk_RLX0000000000000000000001","task_id":"t1","operation_id":"op1","status":"completed"}' +url = "https://buyer.example.com/adcp/webhook/create_media_buy/agent_123/op_abc" +signed = sign_webhook( + method="POST", url=url, + headers={"Content-Type": "application/json"}, + body=body, private_key=priv, key_id=k["kid"], alg="ed25519", + created=1776520800, nonce="RLXnonce0000000000000", +) +hdrs = dict(signed.as_dict()) +def alphabet(tok): + m = re.match(r'^[^:]*:([^:]*):\s*$', (tok or "").strip()) + v = m.group(1) if m else (tok or "") + bits = [] + if re.search(r'[-_]', v): bits.append("url(-_)") + if re.search(r'[+/]', v): bits.append("std(+/)") + if "=" in v: bits.append("padded") + return ",".join(bits) or "ambiguous(A-Za-z0-9 only)" +print(json.dumps({ + "emitted_headers": {h: hdrs.get(h) for h in ("Signature", "Signature-Input", "Content-Digest")}, + "signature_alphabet": alphabet(hdrs.get("Signature")), + "content_digest_alphabet": alphabet(hdrs.get("Content-Digest")), + "request": {"method": "POST", "url": url, + "headers": {**{"Content-Type": "application/json"}, **hdrs}, + "body": body.decode()}, +}, indent=1)) diff --git a/scripts/ci/reporting_interop/signing/verifier-server.cjs b/scripts/ci/reporting_interop/signing/verifier-server.cjs new file mode 100644 index 000000000..dc06bdf16 --- /dev/null +++ b/scripts/ci/reporting_interop/signing/verifier-server.cjs @@ -0,0 +1,62 @@ +'use strict'; +// Real separate-process RFC 9421 verifier over real HTTP, backed by a real +// PostgreSQL replay store. Verifies RAW request bytes, never a re-serialized body. +const http = require('node:http'); +const { Pool } = require('pg'); +const { + verifyRequestSignature, StaticJwksResolver, PostgresReplayStore, InMemoryRevocationStore, +} = require('@adcp/sdk/signing/server'); + +const PORT = Number(process.env.PROBE_PORT); +const PG_URL = process.env.PROBE_PG_URL; +const JWKS = JSON.parse(process.env.PROBE_JWKS); // public JWKs +const pool = new Pool({ connectionString: PG_URL, max: 4 }); +const jwks = new StaticJwksResolver(JWKS); +const replayStore = new PostgresReplayStore(pool, { tableName: 'adcp_replay_cache' }); +const revocationStore = new InMemoryRevocationStore(); +const capability = { + supported: true, + covers_content_digest: 'always', + required_for: ['get_reporting_status', 'sync_reporting_receipts'], +}; + +const server = http.createServer((req, res) => { + const chunks = []; + req.on('data', c => chunks.push(c)); + req.on('end', async () => { + const raw = Buffer.concat(chunks); + // Verifier clock is injected per-request by the probe. This is the SDK's own + // documented `now` option, not a global clock patch. + const injected = req.headers['x-probe-now-sec']; + const nowFn = injected === undefined ? undefined : () => Number(injected); + const operation = req.headers['x-probe-operation'] || undefined; + const request = { + method: req.method, + url: `http://127.0.0.1:${PORT}${req.url}`, + headers: req.headers, + body: raw.toString('utf8'), + }; + let out; + try { + const result = await verifyRequestSignature(request, { + capability, jwks, replayStore, revocationStore, + ...(nowFn ? { now: nowFn } : {}), + ...(operation ? { operation } : {}), + adcpVersion: '3.2.0-rc.4', + }); + out = { ok: true, result, rawBodySha256: require('node:crypto').createHash('sha256').update(raw).digest('hex') }; + } catch (e) { + out = { ok: false, error: { name: e.name, code: e.code, message: e.message } }; + } + res.writeHead(200, { 'content-type': 'application/json' }); + res.end(JSON.stringify(out)); + }); +}); +server.listen(PORT, '127.0.0.1', () => process.stdout.write('LISTENING\n')); +const shutdown = async () => { + server.close(); + try { await pool.end(); } catch {} + process.exit(0); +}; +process.on('SIGTERM', shutdown); +process.on('SIGINT', shutdown); diff --git a/scripts/ci/reporting_interop/signing/webhook_receiver_client.py b/scripts/ci/reporting_interop/signing/webhook_receiver_client.py new file mode 100644 index 000000000..ca57c81e2 --- /dev/null +++ b/scripts/ci/reporting_interop/signing/webhook_receiver_client.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +"""Drive the real-socket stale revocation receiver through public signing.""" + +from __future__ import annotations + +import argparse +import base64 +import json +import sys +import urllib.error +import urllib.request +from pathlib import Path +from typing import Any + +from cryptography.hazmat.primitives.asymmetric import ed25519 + +from adcp.signing.webhook_signer import sign_webhook + +REFERENCE_NOW = 1_776_520_800 +KID = "test-ed25519-webhook-2026" +VECTOR_019_ERROR_CODE = "webhook_signature_revocation_stale" + + +def _arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--receiver-url", required=True) + parser.add_argument("--vector-dir", required=True, type=Path) + parser.add_argument( + "--allow-historical-red", + action="store_true", + help=( + "Exit zero only when the known historical classification failure is reproduced. " + "This mode is diagnostic and can never satisfy a blocking matrix cell." + ), + ) + return parser.parse_args() + + +def _post( + url: str, body: bytes, headers: dict[str, str] +) -> tuple[int, dict[str, Any], dict[str, str]]: + request = urllib.request.Request(url, data=body, headers=headers, method="POST") + try: + with urllib.request.urlopen(request, timeout=10) as response: # noqa: S310 + return ( + response.status, + json.loads(response.read()), + {name.lower(): value for name, value in response.headers.items()}, + ) + except urllib.error.HTTPError as error: + return ( + error.code, + json.loads(error.read()), + {name.lower(): value for name, value in error.headers.items()}, + ) + + +def _control(receiver_url: str, state: str) -> dict[str, Any]: + url = receiver_url.rsplit("/", 1)[0] + "/control" + status, response, _headers = _post( + url, + json.dumps({"state": state}, separators=(",", ":")).encode(), + {"Content-Type": "application/json"}, + ) + if status != 200: + raise RuntimeError(f"receiver control failed: {status} {response}") + return response + + +def _disposition( + *, + normative_match: bool, + receiver_http_mapping_match: bool, + historical_red_reproduced: bool, + allow_historical_red: bool, +) -> tuple[str, bool, int]: + """Keep diagnostic completion distinct from blocking acceptance.""" + blocking_acceptance = normative_match and receiver_http_mapping_match + if allow_historical_red: + if historical_red_reproduced and not blocking_acceptance: + return "historical_red_reproduced", False, 0 + return "historical_red_not_reproduced", False, 1 + if blocking_acceptance: + return "passed", True, 0 + return "blocking_red", False, 1 + + +def main() -> None: + args = _arguments() + keys = json.loads((args.vector_dir / "keys.json").read_text(encoding="utf-8"))["keys"] + key = next(item for item in keys if item["kid"] == KID) + private_bytes = base64.urlsafe_b64decode( + key["_private_d_for_test_only"] + "=" * (-len(key["_private_d_for_test_only"]) % 4) + ) + private_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_bytes) + rows: list[dict[str, Any]] = [] + states = ["fresh", "outage_stale", "refreshed"] + for index, state in enumerate(states, start=1): + control = _control(args.receiver_url, state) + body = json.dumps( + { + "idempotency_key": f"whk_RLXRECEIVER000000000000{index}", + "task_id": f"task-receiver-{index}", + "task_type": "sync_reporting_status", + "protocol": "media-buy", + "operation_id": "op-receiver", + "status": "completed", + "timestamp": "2026-04-18T14:00:00Z", + }, + separators=(",", ":"), + ).encode() + signed = sign_webhook( + method="POST", + url=args.receiver_url, + headers={"Content-Type": "application/json"}, + body=body, + private_key=private_key, + key_id=KID, + alg="ed25519", + created=REFERENCE_NOW, + nonce=f"RLXreceiverNonce{index:02d}", + ) + headers = {"Content-Type": "application/json", **dict(signed.as_dict())} + status, observed, response_headers = _post(args.receiver_url, body, headers) + rows.append( + { + "state": state, + "control": control, + "http_status": status, + "http_response_headers": response_headers, + "observed": observed, + } + ) + + fresh, stale, recovered = rows + if fresh["http_status"] != 200 or fresh["observed"].get("accepted") is not True: + raise RuntimeError(f"fresh receiver control failed: {fresh}") + expected_authenticate = f'Signature error="{VECTOR_019_ERROR_CODE}"' + observed_authenticate = stale["http_response_headers"].get("www-authenticate") + outcome_authenticate = stale["observed"].get("response_headers", {}).get("WWW-Authenticate") + prefix = 'Signature error="' + observed_signature_code = ( + observed_authenticate[len(prefix) : -1] + if isinstance(observed_authenticate, str) + and observed_authenticate.startswith(prefix) + and observed_authenticate.endswith('"') + else None + ) + stale_vector_match = observed_signature_code == VECTOR_019_ERROR_CODE + stale_receiver_http_mapping_match = ( + stale_vector_match + and stale["http_status"] == 401 + and outcome_authenticate == observed_authenticate + ) + operational_error = stale["observed"].get("operational_error", {}) + operational_message = operational_error.get("message") + stale_historical_red = ( + stale["http_status"] == 503 + and operational_error.get("name") == "RevocationListFreshnessError" + and isinstance(operational_message, str) + and "past next_update" in operational_message + and "last refresh error" in operational_message + and "code" not in operational_error + and "step" not in operational_error + and observed_authenticate is None + and outcome_authenticate is None + ) + if not stale_vector_match and not stale_historical_red: + raise RuntimeError(f"stale revocation did not fail closed: {stale}") + if recovered["http_status"] != 200 or recovered["observed"].get("accepted") is not True: + raise RuntimeError(f"receiver did not recover after a fresh fetch: {recovered}") + final_state = recovered["observed"]["state"] + if final_state["fetch_attempts"] != 3 or final_state["successful_fetches"] != 2: + raise RuntimeError(f"unexpected live fetch accounting: {final_state}") + status, blocking_acceptance, exit_code = _disposition( + normative_match=stale_vector_match, + receiver_http_mapping_match=stale_receiver_http_mapping_match, + historical_red_reproduced=stale_historical_red, + allow_historical_red=args.allow_historical_red, + ) + print( + json.dumps( + { + "kind": "reporting_interop_webhook_receiver_refresh", + "status": status, + "execution_completed": True, + "blocking_acceptance": blocking_acceptance, + "historical_reproducer_mode": args.allow_historical_red, + "historical_red_reproduced": stale_historical_red, + "normative_019_match": stale_vector_match, + "vector_019": { + "expected_error_code": VECTOR_019_ERROR_CODE, + "observed_error_code": observed_signature_code, + "error_code_match": stale_vector_match, + "expected_failed_step": 9, + "observed_failed_step": None, + "failed_step_note": "not exposed by the receiver HTTP contract", + "receiver_http_mapping_match": stale_receiver_http_mapping_match, + "observed_http_status": stale["http_status"], + "observed_www_authenticate": observed_authenticate, + "outcome_www_authenticate": outcome_authenticate, + "expected_www_authenticate": expected_authenticate, + }, + "stale_failure_classification": ( + VECTOR_019_ERROR_CODE + if stale_vector_match + else "operational_RevocationListFreshnessError" + ), + "transport": "real_loopback_http_raw_body", + "replay_persistence_exercised": False, + "public_composition": [ + "WebhookReceiver", + "WebhookVerifyOptions", + "CachingRevocationChecker", + "RevocationListFetcher", + ], + "rows": rows, + "limitations": [ + "deterministic_public_test_key", + "loopback_http_test_transport", + "historical_python_signer_profile_does_not_close_cross_language_interop", + "not_reporting_delivery_retry_or_account_activity_evidence", + ], + }, + sort_keys=True, + ) + ) + if exit_code: + sys.exit(exit_code) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/reporting_interop/signing/webhook_receiver_server.py b/scripts/ci/reporting_interop/signing/webhook_receiver_server.py new file mode 100644 index 000000000..c0c3b2882 --- /dev/null +++ b/scripts/ci/reporting_interop/signing/webhook_receiver_server.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +"""Real-socket public webhook receiver with live revocation refresh state. + +The receiver and revocation issuer bind distinct loopback sockets. Verification +uses the installed SDK's public ``WebhookReceiver`` and +``CachingRevocationChecker``. The HTTP fetcher below is a deterministic test +transport implementing the public ``RevocationListFetcher`` protocol; it still +performs an actual HTTP request to the issuer for every refresh attempt. +""" + +from __future__ import annotations + +import argparse +import base64 +import json +import threading +import urllib.error +import urllib.request +from datetime import datetime, timezone +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from importlib.metadata import version +from pathlib import Path +from typing import Any + +from cryptography.hazmat.primitives.asymmetric import ed25519 + +from adcp.server.idempotency import MemoryBackend, WebhookDedupStore +from adcp.signing import CachingRevocationChecker, StaticJwksResolver +from adcp.signing.revocation_fetcher import FetchResult, RevocationListFetchError +from adcp.signing.webhook_verifier import WebhookVerifyOptions +from adcp.webhooks import WebhookReceiver, WebhookReceiverConfig + +REFERENCE_NOW = 1_776_520_800 +ISSUER = "https://governance.example.test" +KID = "test-ed25519-webhook-2026" + + +def _arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--receiver-port", required=True, type=int) + parser.add_argument("--issuer-port", required=True, type=int) + parser.add_argument("--vector-dir", required=True, type=Path) + return parser.parse_args() + + +def _b64url(value: bytes) -> str: + return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii") + + +class State: + def __init__(self, *, key: dict[str, Any]) -> None: + private_bytes = base64.urlsafe_b64decode( + key["_private_d_for_test_only"] + "=" * (-len(key["_private_d_for_test_only"]) % 4) + ) + self.private_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_bytes) + self.lock = threading.Lock() + self.mode = "fresh" + self.wall_seconds = REFERENCE_NOW + self.monotonic_seconds = 0.0 + self.fetch_attempts = 0 + self.successful_fetches = 0 + self.callbacks = 0 + + def transition(self, mode: str) -> dict[str, Any]: + with self.lock: + if mode == "fresh": + self.mode = mode + self.wall_seconds = REFERENCE_NOW + self.monotonic_seconds = 0.0 + elif mode == "outage_stale": + self.mode = mode + self.wall_seconds = REFERENCE_NOW + 500 + self.monotonic_seconds = 120.0 + elif mode == "refreshed": + self.mode = mode + self.wall_seconds = REFERENCE_NOW + 500 + self.monotonic_seconds = 240.0 + else: + raise ValueError(f"unknown receiver state: {mode}") + return self.snapshot() + + def snapshot(self) -> dict[str, Any]: + return { + "mode": self.mode, + "wall_seconds": self.wall_seconds, + "monotonic_seconds": self.monotonic_seconds, + "fetch_attempts": self.fetch_attempts, + "successful_fetches": self.successful_fetches, + "callbacks": self.callbacks, + } + + def revocation_document(self) -> str: + with self.lock: + if self.mode == "outage_stale": + raise RevocationListFetchError("fixture issuer is unavailable") + if self.mode == "refreshed": + updated = REFERENCE_NOW + 480 + next_update = REFERENCE_NOW + 660 + else: + updated = REFERENCE_NOW - 60 + next_update = REFERENCE_NOW + 60 + payload = { + "version": 1, + "issuer": ISSUER, + "updated": datetime.fromtimestamp(updated, timezone.utc) + .isoformat() + .replace("+00:00", "Z"), + "next_update": datetime.fromtimestamp(next_update, timezone.utc) + .isoformat() + .replace("+00:00", "Z"), + "revoked_kids": [], + "revoked_jtis": [], + } + protected = _b64url( + json.dumps( + {"alg": "EdDSA", "kid": KID, "typ": "adcp-gov-revocation+jws"}, + separators=(",", ":"), + sort_keys=True, + ).encode() + ) + encoded_payload = _b64url( + json.dumps(payload, separators=(",", ":"), sort_keys=True).encode() + ) + signature = _b64url( + self.private_key.sign(f"{protected}.{encoded_payload}".encode("ascii")) + ) + return f"{protected}.{encoded_payload}.{signature}" + + +def _json_response(handler: BaseHTTPRequestHandler, status: int, payload: object) -> None: + body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + handler.send_response(status) + handler.send_header("Content-Type", "application/json") + handler.send_header("Content-Length", str(len(body))) + handler.end_headers() + handler.wfile.write(body) + + +def main() -> None: + args = _arguments() + keys = json.loads((args.vector_dir / "keys.json").read_text(encoding="utf-8"))["keys"] + key = next(item for item in keys if item["kid"] == KID) + public_key = {name: value for name, value in key.items() if not name.startswith("_")} + state = State(key=key) + + class IssuerHandler(BaseHTTPRequestHandler): + def do_GET(self) -> None: # noqa: N802 + if self.path != "/.well-known/governance-revocations.json": + _json_response(self, 404, {"error": "not_found"}) + return + with state.lock: + state.fetch_attempts += 1 + outage = state.mode == "outage_stale" + if outage: + _json_response(self, 503, {"error": "temporarily_unavailable"}) + return + document = state.revocation_document().encode() + with state.lock: + state.successful_fetches += 1 + self.send_response(200) + self.send_header("Content-Type", "application/jose") + self.send_header("ETag", f'"{state.mode}"') + self.send_header("Content-Length", str(len(document))) + self.end_headers() + self.wfile.write(document) + + def log_message(self, _format: str, *_args: object) -> None: + return + + issuer = ThreadingHTTPServer(("127.0.0.1", args.issuer_port), IssuerHandler) + issuer_thread = threading.Thread(target=issuer.serve_forever, daemon=True) + issuer_thread.start() + + def fetcher( + uri: str, + *, + if_none_match: str | None = None, + if_modified_since: str | None = None, + ) -> FetchResult: + headers = {"Accept": "application/jose"} + if if_none_match: + headers["If-None-Match"] = if_none_match + if if_modified_since: + headers["If-Modified-Since"] = if_modified_since + try: + with urllib.request.urlopen( # noqa: S310 - fixed loopback fixture URI + urllib.request.Request(uri, headers=headers), timeout=3 + ) as response: + body = response.read().decode("utf-8") + return FetchResult( + body=body, + etag=response.headers.get("ETag"), + last_modified=response.headers.get("Last-Modified"), + ) + except (urllib.error.URLError, TimeoutError) as error: + raise RevocationListFetchError(f"live fixture fetch failed: {error}") from error + + resolver = StaticJwksResolver({"keys": [public_key]}) + checker = CachingRevocationChecker( + revocation_uri=( + f"http://127.0.0.1:{args.issuer_port}/.well-known/governance-revocations.json" + ), + issuer=ISSUER, + jwks_resolver=resolver, + fetcher=fetcher, + clock=lambda: state.monotonic_seconds, + wall_clock=lambda: datetime.fromtimestamp(state.wall_seconds, timezone.utc), + ) + receiver = WebhookReceiver( + WebhookReceiverConfig( + verify_options=WebhookVerifyOptions( + jwks_resolver=resolver, + replay_store=None, + revocation_checker=checker, + clock=lambda: float(REFERENCE_NOW), + ), + dedup=WebhookDedupStore(MemoryBackend(), ttl_seconds=86_400), + receiver_scope="reporting-interop-receiver", + publisher_scope_for=lambda _signer: "test-publisher", + ) + ) + + class ReceiverHandler(BaseHTTPRequestHandler): + def do_POST(self) -> None: # noqa: N802 + length = int(self.headers.get("Content-Length", "0")) + body = self.rfile.read(length) + if self.path == "/control": + try: + requested = json.loads(body)["state"] + snapshot = state.transition(requested) + except (KeyError, TypeError, ValueError, json.JSONDecodeError) as error: + _json_response(self, 400, {"error": str(error)}) + return + _json_response(self, 200, snapshot) + return + if self.path != "/webhook": + _json_response(self, 404, {"error": "not_found"}) + return + with state.lock: + state.callbacks += 1 + signed_url = f"http://127.0.0.1:{args.receiver_port}/webhook" + try: + outcome = receiver.receive_and_process_sync( + method="POST", + url=signed_url, + headers=dict(self.headers), + body=body, + handler=lambda _payload: None, + ) + except Exception as error: # noqa: BLE001 - retain public composition failures + _json_response( + self, + 503, + { + "accepted": False, + "operational_error": { + "name": type(error).__name__, + "message": str(error), + }, + "state": state.snapshot(), + }, + ) + return + status = outcome.http_status or 200 + payload = { + "accepted": not outcome.rejected and not outcome.in_progress, + "duplicate": outcome.duplicate, + "handled": outcome.handled, + "http_status": status, + "rejection_reason": outcome.rejection_reason, + "response_headers": dict(outcome.response_headers), + "state": state.snapshot(), + } + response = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + self.send_response(status) + for name, value in outcome.response_headers.items(): + self.send_header(name, value) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(response))) + self.end_headers() + self.wfile.write(response) + + def log_message(self, _format: str, *_args: object) -> None: + return + + server = ThreadingHTTPServer(("127.0.0.1", args.receiver_port), ReceiverHandler) + print( + json.dumps( + { + "kind": "reporting_interop_webhook_receiver_ready", + "package": {"name": "adcp", "version": version("adcp")}, + "receiver_url": f"http://127.0.0.1:{args.receiver_port}/webhook", + "issuer_url": ( + f"http://127.0.0.1:{args.issuer_port}/.well-known/governance-revocations.json" + ), + }, + sort_keys=True, + ), + flush=True, + ) + try: + server.serve_forever() + finally: + server.server_close() + issuer.shutdown() + issuer.server_close() + issuer_thread.join(timeout=3) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/reporting_interop/signing/webhook_revocation_state.py b/scripts/ci/reporting_interop/signing/webhook_revocation_state.py new file mode 100644 index 000000000..841d193f9 --- /dev/null +++ b/scripts/ci/reporting_interop/signing/webhook_revocation_state.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Exercise rc.4 vector 019 with the public revocation-list receiver state.""" + +from __future__ import annotations + +import json +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from adcp.signing.errors import SignatureVerificationError +from adcp.signing.revocation import RevocationList +from adcp.signing.webhook_verifier import WebhookVerifyOptions, verify_webhook_signature + + +def _options(vector: dict, keys: dict[str, dict], *, stale: bool) -> WebhookVerifyOptions: + now = datetime.fromtimestamp(vector["reference_now"], tz=timezone.utc) + next_update = now - timedelta(seconds=10800) if stale else now + timedelta(seconds=3600) + revocations = RevocationList( + issuer="https://receiver.example.test", + updated=(now - timedelta(seconds=14400)).isoformat(), + next_update=next_update.isoformat(), + ) + return WebhookVerifyOptions( + jwks_resolver=lambda kid: keys.get(kid), + revocation_list=revocations, + clock=lambda: float(vector["reference_now"]), + ) + + +def _run(vector: dict, keys: dict[str, dict], *, stale: bool) -> dict: + request = vector["request"] + try: + verify_webhook_signature( + method=request["method"], + url=request["url"], + headers=request["headers"], + body=request["body"].encode(), + options=_options(vector, keys, stale=stale), + ) + return {"success": True} + except SignatureVerificationError as error: + return { + "success": False, + "error_code": error.code, + "step": error.step, + } + + +def main() -> None: + vector_dir = Path(sys.argv[1]) + vector = json.loads( + (vector_dir / "negative" / "019-revocation-stale.json").read_text(encoding="utf-8") + ) + keys = { + key["kid"]: {name: value for name, value in key.items() if not name.startswith("_")} + for key in json.loads((vector_dir / "keys.json").read_text(encoding="utf-8"))["keys"] + } + report = { + "kind": "protocol_rc4_webhook_revocation_state_control", + "vector": "negative/019-revocation-stale.json", + "requires_contract": vector["requires_contract"], + "mode": "configured_in_process_public_revocation_list", + "fresh": _run(vector, keys, stale=False), + "stale": _run(vector, keys, stale=True), + "limitations": [ + "not_real_stale_fetch", + "not_live_polling_or_refresh_recovery", + "not_socket_callback_evidence", + ], + } + print(json.dumps(report, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/reporting_interop/signing/webhook_vectors_python.original.py.txt b/scripts/ci/reporting_interop/signing/webhook_vectors_python.original.py.txt new file mode 100644 index 000000000..23a53def6 --- /dev/null +++ b/scripts/ci/reporting_interop/signing/webhook_vectors_python.original.py.txt @@ -0,0 +1,58 @@ +"""RLX-XL-002: run the protocol's own rc.4 webhook-signing vectors through the +installed Python public verifier. Neutral corpus; no invented inputs.""" +from __future__ import annotations +import json, sys, glob, os +from pathlib import Path + +from adcp.signing.webhook_verifier import WebhookVerifyOptions, verify_webhook_signature +from adcp.signing.errors import SignatureVerificationError +from adcp.signing.replay import InMemoryReplayStore + +VEC = Path(sys.argv[1]) +keys = json.loads((VEC / "keys.json").read_text())["keys"] +BY_KID = {k["kid"]: {kk: vv for kk, vv in k.items() if not kk.startswith("_")} for k in keys} + +def run(path: Path): + v = json.loads(path.read_text()) + req = v["request"] + ref_now = v.get("reference_now") + override = v.get("jwks_override") + table = dict(BY_KID) + if override: + # Observed shape is a {kid: jwk} mapping. + for kid, jwk in override.items(): + table[kid] = jwk + state = v.get("test_harness_state") or {} + cap_for = state.get("per_keyid_cap_filled_for") + # Mirror the TS harness: express the vector's per-keyid cap through the + # public store's own per_keyid_cap knob, then fill it for the named keyid. + replay = InMemoryReplayStore(per_keyid_cap=1) if cap_for else InMemoryReplayStore() + if cap_for: + replay.remember(cap_for, "rlx-prefill-nonce", 3600) + for entry in state.get("replay_cache_entries", []): + replay.remember(entry["keyid"], entry["nonce"], 3600) + revoked = set(state.get("revoked_kids", [])) + opts = WebhookVerifyOptions( + jwks_resolver=lambda kid: table.get(kid), + replay_store=replay, + revocation_checker=(lambda kid: kid in revoked) if revoked else None, + **({"clock": (lambda: float(ref_now))} if ref_now is not None else {}), + ) + body = req.get("body", "") + body_bytes = body.encode() if isinstance(body, str) else bytes(body) + try: + verify_webhook_signature(method=req["method"], url=req["url"], + headers=req["headers"], body=body_bytes, options=opts) + got = {"success": True} + except SignatureVerificationError as e: + got = {"success": False, "error_code": getattr(e, "code", None) or str(e)[:60]} + except Exception as e: # noqa: BLE001 + got = {"success": False, "error_code": f"UNEXPECTED:{type(e).__name__}", "detail": str(e)[:90]} + exp = v["expected_outcome"] + ok = got["success"] == exp["success"] and ( + exp["success"] or got.get("error_code") == exp.get("error_code")) + return {"vector": path.parent.name + "/" + path.name, "requires_contract": v.get("requires_contract"), + "expected": exp, "got": got, "match": ok} + +rows = [run(Path(p)) for p in sorted(glob.glob(str(VEC / "positive" / "*.json")) + glob.glob(str(VEC / "negative" / "*.json")))] +print(json.dumps(rows, indent=1)) diff --git a/scripts/ci/reporting_interop/signing/webhook_vectors_typescript.cjs b/scripts/ci/reporting_interop/signing/webhook_vectors_typescript.cjs new file mode 100644 index 000000000..2cd4c09d8 --- /dev/null +++ b/scripts/ci/reporting_interop/signing/webhook_vectors_typescript.cjs @@ -0,0 +1,58 @@ +'use strict'; +// RLX-XL-002 TS side: protocol rc.4 webhook-signing vectors through the installed public verifier. +const fs=require('fs'), path=require('path'), Module=require('node:module'); +const base=path.resolve(process.argv[2]); +const req=Module.createRequire(path.join(base,'noop.js')); +const VEC=path.resolve(process.argv[3]); +const S=req('@adcp/sdk/signing'); +const keys=JSON.parse(fs.readFileSync(path.join(VEC,'keys.json'),'utf8')).keys + .map(k=>Object.fromEntries(Object.entries(k).filter(([n])=>!n.startsWith('_')))); +const BY_KID=Object.fromEntries(keys.map(k=>[k.kid,k])); + +function makeReplay(entries, capFilledFor){ + const seen=new Set(); + for(const e of entries||[]) seen.add(e.keyid+'|*|'+e.nonce); + return { + async has(keyid,scope,nonce){ return seen.has(keyid+'|*|'+nonce); }, + async isCapHit(keyid){ return capFilledFor===keyid; }, + async insert(keyid,scope,nonce){ + if(capFilledFor===keyid) return 'rate_abuse'; + const k=keyid+'|*|'+nonce; + if(seen.has(k)) return 'replayed'; + seen.add(k); return 'ok'; + }, + }; +} + +async function run(file){ + const v=JSON.parse(fs.readFileSync(file,'utf8')); + const r=v.request, state=v.test_harness_state||{}; + const table={...BY_KID, ...(v.jwks_override||{})}; + const revoked=new Set(state.revoked_kids||[]); + const opts={ + jwks: { async resolve(kid){ return table[kid] ?? null; } }, + replayStore: makeReplay(state.replay_cache_entries, state.per_keyid_cap_filled_for), + revocationStore: { async isRevoked(kid){ return revoked.has(kid); } }, + ...(v.reference_now!==undefined ? { now: ()=>v.reference_now } : {}), + }; + let got; + try{ + await S.verifyWebhookSignature({method:r.method,url:r.url,headers:r.headers,body:r.body??''},opts); + got={success:true}; + }catch(e){ + got={success:false, error_code: e?.code ?? e?.reason ?? (e?.message||'').slice(0,60)}; + } + const exp=v.expected_outcome; + const match = got.success===exp.success && (exp.success || got.error_code===exp.error_code); + return {vector: path.basename(path.dirname(file))+'/'+path.basename(file), + requires_contract: v.requires_contract ?? null, expected: exp, got, match}; +} + +(async()=>{ + const files=[] + .concat(fs.readdirSync(path.join(VEC,'positive')).sort().map(f=>path.join(VEC,'positive',f))) + .concat(fs.readdirSync(path.join(VEC,'negative')).sort().map(f=>path.join(VEC,'negative',f))) + .filter(f=>f.endsWith('.json')); + const rows=[]; for(const f of files) rows.push(await run(f)); + process.stdout.write(JSON.stringify(rows,null,1)+'\n'); +})().catch(e=>{process.stderr.write('RUNNER: '+e.stack+'\n');process.exitCode=2;}); diff --git a/scripts/ci/reporting_interop/storyboard_orchestration.py b/scripts/ci/reporting_interop/storyboard_orchestration.py new file mode 100644 index 000000000..bd6c65f16 --- /dev/null +++ b/scripts/ci/reporting_interop/storyboard_orchestration.py @@ -0,0 +1,1440 @@ +#!/usr/bin/env python3 +"""Auditable lifecycle for the five Reliable Reporting storyboards. + +The default invocation validates inputs and prints a plan. It never starts a +process. ``--execute`` is deliberately required before PostgreSQL, seller +processes, or the installed CLI can run. Even a complete storyboard run is +not matrix or release acceptance; the aggregate gate owns that decision. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import os +import secrets +import shutil +import signal +import socket +import subprocess +import sys +import tarfile +import tempfile +import time +from collections import Counter +from collections.abc import Callable, Iterable, Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Any +from urllib.parse import urlsplit + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[2] +PINS = HERE / "pins.json" +STORYBOARD_PRIVATE_ENV = { + "auth_token": "ADCP_INTEROP_TS_STORYBOARD_AUTH_TOKEN", + "startup_proof": "ADCP_INTEROP_TS_STORYBOARD_STARTUP_PROOF", +} + +REQUIRED_STORYBOARDS: tuple[tuple[str, int, int, str], ...] = ( + ("reliable_reporting_managed_delivery", 6, 5, "managed"), + ("reliable_reporting_reconciled_billing", 16, 15, "billing"), + ("reporting_consumer_status", 43, 41, "controlled"), + ("reporting_core", 23, 23, "controlled"), + ("reporting_core_declaration", 1, 0, "controlled"), +) +EXPECTED_TOTALS = {"storyboards": 5, "steps": 89, "stateful_steps": 84} + + +class OrchestrationError(RuntimeError): + """A fail-closed preparation or execution error.""" + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _positive_seconds(value: str) -> float: + number = float(value) + if not 0 < number <= 3_600: + raise argparse.ArgumentTypeError("timeout must be greater than zero and at most 3600") + return number + + +def _port(value: str) -> int: + number = int(value) + if not 1_024 <= number <= 65_535: + raise argparse.ArgumentTypeError("port must be between 1024 and 65535") + return number + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--node-runtime", type=Path, required=True) + parser.add_argument("--node-sha256", required=True) + parser.add_argument("--typescript-install", type=Path, required=True) + parser.add_argument("--typescript-tarball", type=Path, required=True) + parser.add_argument( + "--typescript-role", + choices=("candidate", "historical_candidate", "previous", "lead"), + required=True, + ) + parser.add_argument("--inventory-json", type=Path, required=True) + parser.add_argument("--output", type=Path) + database = parser.add_mutually_exclusive_group(required=True) + database.add_argument("--external-pg-admin-dsn") + database.add_argument("--local-pg-bin", type=Path) + parser.add_argument("--allow-non-loopback-external-dsn", action="store_true") + parser.add_argument("--local-pg-port", type=_port, default=55439) + parser.add_argument("--service-port-base", type=_port, default=55500) + parser.add_argument("--startup-timeout", type=_positive_seconds, default=60.0) + parser.add_argument("--storyboard-timeout", type=_positive_seconds, default=240.0) + parser.add_argument("--shutdown-timeout", type=_positive_seconds, default=10.0) + parser.add_argument("--execute", action="store_true") + return parser.parse_args(argv) + + +@dataclass(frozen=True) +class Case: + storyboard_id: str + step_count: int + stateful_step_count: int + server_kind: str + + +@dataclass(frozen=True) +class FixedInputs: + role: str + node: Path + node_sha256: str + install: Path + tarball: Path + tarball_sha256: str + package_member_count: int + package_member_manifest_sha256: str + lock: Path + lock_sha256: str + version: str + integrity: str + adcp_version: str + inventory: Mapping[str, Any] + cases: tuple[Case, ...] + + +def _load_json(path: Path) -> Any: + try: + return json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise OrchestrationError(f"cannot read valid JSON from {path}: {error}") from error + + +def _inventory_rows(inventory: Mapping[str, Any]) -> list[Mapping[str, Any]]: + rows = inventory.get("inventory") + if not isinstance(rows, list): + raise OrchestrationError("inventory JSON omitted its inventory rows") + return rows + + +def validate_inventory(inventory: Mapping[str, Any]) -> tuple[Case, ...]: + rows = _inventory_rows(inventory) + if len(rows) != len(REQUIRED_STORYBOARDS) or any(not isinstance(row, dict) for row in rows): + raise OrchestrationError( + f"storyboard inventory must contain exactly {len(REQUIRED_STORYBOARDS)} rows" + ) + by_id = {row.get("id"): row for row in rows if isinstance(row, dict)} + required_ids = [item[0] for item in REQUIRED_STORYBOARDS] + if list(by_id) != required_ids: + raise OrchestrationError( + f"storyboard inventory must contain the exact ordered denominator {required_ids}; " + f"got {list(by_id)}" + ) + cases: list[Case] = [] + for storyboard_id, steps, stateful, server_kind in REQUIRED_STORYBOARDS: + row = by_id[storyboard_id] + identities = row.get("steps") + if not isinstance(identities, list) or len(identities) != steps: + raise OrchestrationError(f"{storyboard_id} must retain {steps} literal step identities") + observed_ids = [identity.get("id") for identity in identities if isinstance(identity, dict)] + if ( + len(observed_ids) != steps + or len(set(observed_ids)) != steps + or any(not isinstance(value, str) or not value for value in observed_ids) + ): + raise OrchestrationError(f"{storyboard_id} has missing or duplicate literal step IDs") + if row.get("step_count") != steps or row.get("stateful_step_count") != stateful: + raise OrchestrationError( + f"{storyboard_id} count drift: expected {steps}/{stateful}, " + f"got {row.get('step_count')}/{row.get('stateful_step_count')}" + ) + cases.append(Case(storyboard_id, steps, stateful, server_kind)) + totals = inventory.get("totals") + if totals != EXPECTED_TOTALS: + raise OrchestrationError( + f"storyboard totals drifted: expected {EXPECTED_TOTALS}, got {totals}" + ) + return tuple(cases) + + +def _is_loopback(hostname: str | None) -> bool: + return hostname in {"127.0.0.1", "::1", "localhost"} + + +def validate_external_dsn(dsn: str, *, allow_non_loopback: bool) -> None: + parsed = urlsplit(dsn) + if parsed.scheme not in {"postgres", "postgresql"} or not parsed.hostname: + raise OrchestrationError("external PostgreSQL DSN must be a postgresql:// URL") + if not allow_non_loopback and not _is_loopback(parsed.hostname): + raise OrchestrationError( + "external PostgreSQL DSN must be loopback unless explicitly authorized" + ) + if parsed.path in {"", "/"}: + raise OrchestrationError("external PostgreSQL DSN must name an administrative database") + + +def _typescript_pin(role: str) -> Mapping[str, Any]: + pins = _load_json(PINS) + key = { + "candidate": "development_candidate_rc47", + "historical_candidate": "historical_candidate_rc45", + "previous": "previous_compatible", + "lead": "newer_rc_lead", + }[role] + try: + return pins["typescript"][key] + except (KeyError, TypeError) as error: + raise OrchestrationError(f"pins.json omits the TypeScript {role} pin") from error + + +def _manifest_sha256(entries: Mapping[str, str]) -> str: + encoded = json.dumps(entries, sort_keys=True, separators=(",", ":")).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + +def validate_package_archive( + tarball: Path, install: Path, pin: Mapping[str, Any] +) -> tuple[int, str]: + """Bind every published package member to the installed package bytes.""" + + if not tarball.is_file(): + raise OrchestrationError(f"TypeScript registry tarball is unavailable: {tarball}") + observed_tarball_sha256 = sha256(tarball) + if observed_tarball_sha256 != pin["tarball_sha256"]: + raise OrchestrationError( + "TypeScript tarball hash mismatch: " + f"expected {pin['tarball_sha256']}, got {observed_tarball_sha256}" + ) + archive: dict[str, str] = {} + with tarfile.open(tarball, "r:gz") as stream: + members = stream.getmembers() + for member in members: + if member.isdir(): + continue + if not member.isfile() or not member.name.startswith("package/"): + raise OrchestrationError( + f"TypeScript tarball contains unsupported member {member.name!r}" + ) + name = member.name.removeprefix("package/") + if not name or name.startswith("/") or ".." in Path(name).parts or name in archive: + raise OrchestrationError(f"unsafe or duplicate TypeScript member {member.name!r}") + extracted = stream.extractfile(member) + if extracted is None: + raise OrchestrationError(f"cannot read TypeScript member {member.name!r}") + archive[name] = hashlib.sha256(extracted.read()).hexdigest() + expected_count = pin.get("tarball_members") + if expected_count is not None and len(archive) != expected_count: + raise OrchestrationError( + "TypeScript archive member count mismatch: " + f"expected {expected_count}, got {len(archive)}" + ) + package_root = install / "node_modules/@adcp/sdk" + installed = { + path.relative_to(package_root).as_posix(): sha256(path) + for path in sorted(package_root.rglob("*")) + if path.is_file() + } + if archive != installed: + missing = sorted(set(archive) - set(installed)) + unexpected = sorted(set(installed) - set(archive)) + changed = sorted( + name for name in set(archive) & set(installed) if archive[name] != installed[name] + ) + raise OrchestrationError( + "installed TypeScript package differs from registry archive: " + f"missing={missing[:5]} unexpected={unexpected[:5]} changed={changed[:5]}" + ) + return len(archive), _manifest_sha256(archive) + + +def validate_fixed_inputs(args: argparse.Namespace) -> FixedInputs: + node = args.node_runtime.resolve() + install = args.typescript_install.resolve() + tarball = args.typescript_tarball.resolve() + inventory_path = args.inventory_json.resolve() + if not node.is_file(): + raise OrchestrationError(f"Node runtime is unavailable: {node}") + actual_node_hash = sha256(node) + if actual_node_hash != args.node_sha256: + raise OrchestrationError( + f"Node runtime hash mismatch: expected {args.node_sha256}, got {actual_node_hash}" + ) + pin = _typescript_pin(args.typescript_role) + if args.typescript_role == "candidate" and pin.get("executable_harness_input") is not True: + raise OrchestrationError( + "the rc.6 candidate identity is recorded but has no selected executable lock/member " + "binding; historical rc.45 cannot substitute for required Q-cell execution" + ) + lock = install / "package-lock.json" + package = install / "node_modules/@adcp/sdk/package.json" + cli = install / "node_modules/@adcp/sdk/bin/adcp.js" + for path in (lock, package, cli): + if not path.is_file(): + raise OrchestrationError(f"pinned TypeScript install is incomplete: {path}") + lock_hash = sha256(lock) + if lock_hash != pin["package_lock_sha256"]: + raise OrchestrationError( + f"TypeScript lock hash mismatch: expected {pin['package_lock_sha256']}, got {lock_hash}" + ) + lock_data = _load_json(lock) + package_data = _load_json(package) + entry = lock_data.get("packages", {}).get("node_modules/@adcp/sdk", {}) + if ( + entry.get("version") != pin["version"] + or entry.get("integrity") != pin["integrity"] + or package_data.get("version") != pin["version"] + ): + raise OrchestrationError("installed TypeScript package does not match the exact candidate") + member_count, member_manifest = validate_package_archive(tarball, install, pin) + if args.external_pg_admin_dsn: + validate_external_dsn( + args.external_pg_admin_dsn, + allow_non_loopback=args.allow_non_loopback_external_dsn, + ) + else: + pg_bin = args.local_pg_bin.resolve() + for name in ("initdb", "postgres"): + executable = pg_bin / name + if not executable.is_file() or not os.access(executable, os.X_OK): + raise OrchestrationError(f"local PostgreSQL binary is unavailable: {executable}") + inventory = _load_json(inventory_path) + cases = validate_inventory(inventory) + if args.service_port_base + len(cases) - 1 > 65_535: + raise OrchestrationError("service port range exceeds 65535") + return FixedInputs( + role=args.typescript_role, + node=node, + node_sha256=actual_node_hash, + install=install, + tarball=tarball, + tarball_sha256=sha256(tarball), + package_member_count=member_count, + package_member_manifest_sha256=member_manifest, + lock=lock, + lock_sha256=lock_hash, + version=pin["version"], + integrity=pin["integrity"], + adcp_version=pin["protocol"], + inventory=inventory, + cases=cases, + ) + + +@dataclass(frozen=True) +class ProcessIdentity: + pid: int + state: str + process_group: int + session: int + start_token: str + + +def _process_identity(pid: int) -> ProcessIdentity | None: + try: + stat = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8") + except OSError: + return None + closing = stat.rfind(")") + fields = stat[closing + 2 :].split() + if closing < 0 or len(fields) <= 19: + return None + try: + return ProcessIdentity( + pid=pid, + state=fields[0], + process_group=int(fields[2]), + session=int(fields[3]), + start_token=fields[19], + ) + except ValueError: + return None + + +def _process_start_token(pid: int) -> str | None: + identity = _process_identity(pid) + return identity.start_token if identity is not None else None + + +def _same_process(left: ProcessIdentity | None, right: ProcessIdentity) -> bool: + return left is not None and ( + left.pid, + left.process_group, + left.session, + left.start_token, + ) == ( + right.pid, + right.process_group, + right.session, + right.start_token, + ) + + +def _owned_session_members(session: int, leader_start_token: str) -> tuple[ProcessIdentity, ...]: + """Return live members created in the owned, newly-created session.""" + + try: + earliest = int(leader_start_token) + except ValueError as error: + raise OrchestrationError("owned process start token is not numeric") from error + members: list[ProcessIdentity] = [] + try: + entries = tuple(Path("/proc").iterdir()) + except OSError as error: + raise OrchestrationError(f"cannot inspect owned process session: {error}") from error + for entry in entries: + if not entry.name.isdecimal(): + continue + identity = _process_identity(int(entry.name)) + if ( + identity is not None + and identity.state != "Z" + and identity.session == session + and int(identity.start_token) >= earliest + ): + members.append(identity) + return tuple(sorted(members, key=lambda item: (item.pid == session, item.pid))) + + +def assert_process_ownership_supported() -> None: + """Fail before spawn unless Linux supplies both /proc identity and pidfds.""" + + if sys.platform != "linux" or not Path("/proc/self/stat").is_file(): + raise OrchestrationError("owned process execution requires Linux /proc") + if not hasattr(os, "pidfd_open") or not hasattr(signal, "pidfd_send_signal"): + raise OrchestrationError("owned process execution requires Linux pidfd support") + try: + descriptor = os.pidfd_open(os.getpid()) + except OSError as error: + raise OrchestrationError(f"cannot establish pidfd ownership support: {error}") from error + os.close(descriptor) + + +class OwnedProcess: + """A gated process session guarded by exact pidfd and /proc identities.""" + + def __init__( + self, + process: subprocess.Popen[bytes], + *, + identity: ProcessIdentity | None = None, + pidfd: int | None = None, + identity_reader: Callable[[int], ProcessIdentity | None] = _process_identity, + member_reader: Callable[[int, str], tuple[ProcessIdentity, ...]] = _owned_session_members, + pidfd_open: Callable[[int], int] | None = None, + pidfd_signal: Callable[[int, int], None] | None = None, + pidfd_close: Callable[[int], None] = os.close, + ) -> None: + self.process = process + self.pid = process.pid + self._identity_reader = identity_reader + self._member_reader = member_reader + self._pidfd_open = pidfd_open or os.pidfd_open + self._pidfd_signal = pidfd_signal or ( + lambda descriptor, sig: signal.pidfd_send_signal(descriptor, sig) + ) + self._pidfd_close = pidfd_close + self.pidfd = pidfd + self.death_proven = process.poll() is not None + self.identity = identity if identity is not None else identity_reader(self.pid) + if self.identity is None: + raise OrchestrationError(f"cannot establish process ownership for PID {self.pid}") + if self.identity.process_group != self.pid or self.identity.session != self.pid: + raise OrchestrationError( + f"PID {self.pid} does not own the required process group and session" + ) + self.start_token = self.identity.start_token + self.session = self.identity.session + + @classmethod + def spawn( + cls, + command: Sequence[str], + *, + ownership_timeout: float, + popen: Callable[..., subprocess.Popen[bytes]] = subprocess.Popen, + identity_reader: Callable[[int], ProcessIdentity | None] = _process_identity, + member_reader: Callable[[int, str], tuple[ProcessIdentity, ...]] = _owned_session_members, + pidfd_open: Callable[[int], int] | None = None, + pidfd_signal: Callable[[int, int], None] | None = None, + pidfd_close: Callable[[int], None] = os.close, + **kwargs: Any, + ) -> OwnedProcess: + """Claim a blocked launcher before allowing the requested program to exec.""" + + assert_process_ownership_supported() + open_pidfd = pidfd_open or os.pidfd_open + send_pidfd = pidfd_signal or ( + lambda descriptor, sig: signal.pidfd_send_signal(descriptor, sig) + ) + gate_read, gate_write = os.pipe() + inherited = tuple(kwargs.pop("pass_fds", ())) + launcher = ( + "import json,os,sys;" + "fd=int(sys.argv[1]);allowed=os.read(fd,1);os.close(fd);" + "sys.exit(125) if allowed!=b'1' else None;" + "argv=json.loads(sys.argv[2]);os.execvpe(argv[0],argv,os.environ)" + ) + wrapped = [ + sys.executable, + "-I", + "-c", + launcher, + str(gate_read), + json.dumps([os.fspath(item) for item in command]), + ] + try: + process = popen(wrapped, pass_fds=(*inherited, gate_read), **kwargs) + except BaseException: + os.close(gate_read) + os.close(gate_write) + raise + os.close(gate_read) + descriptor: int | None = None + try: + descriptor = open_pidfd(process.pid) + identity = identity_reader(process.pid) + if identity is None: + raise OrchestrationError( + f"cannot establish /proc start identity for spawned PID {process.pid}" + ) + if identity.process_group != process.pid or identity.session != process.pid: + raise OrchestrationError( + f"spawned PID {process.pid} does not own its process group and session" + ) + owned = cls( + process, + identity=identity, + pidfd=descriptor, + identity_reader=identity_reader, + member_reader=member_reader, + pidfd_open=open_pidfd, + pidfd_signal=send_pidfd, + pidfd_close=pidfd_close, + ) + os.write(gate_write, b"1") + os.close(gate_write) + return owned + except Exception as primary: + cleanup_error: Exception | None = None + try: + os.close(gate_write) + except OSError: + pass + try: + if process.poll() is None: + if descriptor is None: + raise OrchestrationError( + f"spawned PID {process.pid} has no safe pidfd cleanup handle" + ) + send_pidfd(descriptor, signal.SIGKILL) + process.wait(timeout=ownership_timeout) + except Exception as error: # noqa: BLE001 - preserve both failures + cleanup_error = error + if descriptor is not None and process.poll() is not None: + pidfd_close(descriptor) + if cleanup_error is not None: + raise OrchestrationError( + f"{primary}; spawned PID {process.pid} death is unproven: {cleanup_error}" + ) from primary + raise OrchestrationError(f"{primary}; spawned child was safely settled") from primary + + def stop( + self, + timeout: float, + ) -> None: + if self.process.poll() is None: + current = self._identity_reader(self.pid) + if not _same_process(current, self.identity): + raise OrchestrationError(f"refusing to signal reused or unowned PID {self.pid}") + self._signal_owned_members(signal.SIGTERM) + if not self._wait_until_settled(timeout): + self._signal_owned_members(signal.SIGKILL) + if not self._wait_until_settled(min(timeout, 5.0)): + raise OrchestrationError( + f"PID {self.pid} and owned session descendants did not exit after SIGKILL" + ) + self.death_proven = self.process.poll() is not None and not self._live_members() + if not self.death_proven: + raise OrchestrationError(f"PID {self.pid} or owned descendants' death is unproven") + self._close_pidfd() + + def _live_members(self) -> tuple[ProcessIdentity, ...]: + members = self._member_reader(self.session, self.start_token) + if self.process.poll() is None and not any( + _same_process(member, self.identity) for member in members + ): + raise OrchestrationError( + f"live PID {self.pid} disappeared from its owned session inventory" + ) + return members + + def _signal_owned_members(self, sig: int) -> None: + for member in self._live_members(): + descriptor: int | None = None + try: + descriptor = self._pidfd_open(member.pid) + if not _same_process(self._identity_reader(member.pid), member): + continue + self._pidfd_signal(descriptor, sig) + except ProcessLookupError: + continue + finally: + if descriptor is not None: + self._pidfd_close(descriptor) + + def _wait_until_settled(self, timeout: float) -> bool: + deadline = time.monotonic() + timeout + while True: + try: + self.process.wait(timeout=0) + except subprocess.TimeoutExpired: + pass + if self.process.poll() is not None and not self._live_members(): + return True + if time.monotonic() >= deadline: + return False + time.sleep(min(0.05, max(0.0, deadline - time.monotonic()))) + + def _close_pidfd(self) -> None: + if self.pidfd is not None: + self._pidfd_close(self.pidfd) + self.pidfd = None + + +class OwnedTempRoot: + """Temporary directory deleted only when its unpredictable owner marker matches.""" + + def __init__(self, parent: Path | None = None) -> None: + parent_value = str(parent.resolve()) if parent else None + self.path = Path(tempfile.mkdtemp(prefix="adcp-storyboard-pg-", dir=parent_value)).resolve() + self.token = secrets.token_hex(24) + self.marker = self.path / ".adcp-storyboard-owner.json" + self.marker.write_text(json.dumps({"token": self.token}) + "\n", encoding="utf-8") + + def cleanup(self) -> None: + if not self.path.exists(): + return + try: + marker = json.loads(self.marker.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise OrchestrationError(f"refusing to clean unmarked temp root {self.path}") from error + if marker != {"token": self.token}: + raise OrchestrationError( + f"refusing to clean temp root with mismatched owner {self.path}" + ) + shutil.rmtree(self.path) + + +class LocalPostgres: + def __init__( + self, + *, + pg_bin: Path, + port: int, + output: Path, + startup_timeout: float, + shutdown_timeout: float, + ) -> None: + self.pg_bin = pg_bin.resolve() + self.port = port + self.output = output + self.startup_timeout = startup_timeout + self.shutdown_timeout = shutdown_timeout + self.temp: OwnedTempRoot | None = None + self.process: OwnedProcess | None = None + self.log_stream: Any = None + self.identity: dict[str, Any] | None = None + + def _dsn(self, socket_directory: Path) -> str: + from psycopg.conninfo import make_conninfo + + fields: dict[str, Any] = { + "host": str(socket_directory), + "port": self.port, + "dbname": "postgres", + } + if os.environ.get("USER"): + fields["user"] = os.environ["USER"] + return make_conninfo(**fields) + + def _wait_owned_ready( + self, dsn: str, data: Path, process: subprocess.Popen[bytes] + ) -> dict[str, Any]: + import psycopg + + deadline = time.monotonic() + self.startup_timeout + last_error: Exception | None = None + while time.monotonic() < deadline: + if process.poll() is not None: + raise OrchestrationError( + f"owned PostgreSQL exited before readiness with {process.returncode}" + ) + try: + with psycopg.connect( + dsn, + connect_timeout=max(1, math.ceil(min(self.startup_timeout, 5))), + options="-c statement_timeout=5000 -c lock_timeout=5000", + ) as connection: + row = connection.execute( + "SELECT current_setting('data_directory'), " + "pg_postmaster_start_time()::text, pg_backend_pid()" + ).fetchone() + if row is None or Path(row[0]).resolve() != data.resolve(): + raise OrchestrationError( + f"PostgreSQL data_directory is not the owned path: {row}" + ) + return { + "data_directory": str(data.resolve()), + "postmaster_started_at": row[1], + "readiness_backend_pid": row[2], + "postmaster_pid": process.pid, + "postmaster_start_token": self.process.start_token if self.process else None, + } + except (psycopg.Error, OSError) as error: + last_error = error + time.sleep(0.05) + raise OrchestrationError(f"owned PostgreSQL readiness timed out: {last_error}") + + def start(self) -> str: + try: + self.temp = OwnedTempRoot() + data = self.temp.path / "data" + socket_directory = self.temp.path / "socket" + socket_directory.mkdir(mode=0o700) + init_exit = _run_owned_to_files( + [ + str(self.pg_bin / "initdb"), + "-D", + str(data), + "--no-locale", + "--encoding=UTF8", + ], + cwd=self.output, + env=_minimal_environment(), + stdout_path=self.output / "postgres-init.stdout.log", + stderr_path=self.output / "postgres-init.stderr.log", + timeout=self.startup_timeout, + shutdown_timeout=self.shutdown_timeout, + ) + if init_exit != 0: + raise OrchestrationError(f"initdb failed with exit {init_exit}") + self.log_stream = (self.output / "postgres.log").open("wb") + self.process = OwnedProcess.spawn( + [ + str(self.pg_bin / "postgres"), + "-D", + str(data), + "-c", + "listen_addresses=", + "-k", + str(socket_directory), + "-p", + str(self.port), + ], + ownership_timeout=self.shutdown_timeout, + stdin=subprocess.DEVNULL, + stdout=self.log_stream, + stderr=subprocess.STDOUT, + start_new_session=True, + ) + dsn = self._dsn(socket_directory) + self.identity = self._wait_owned_ready(dsn, data, self.process.process) + return dsn + except Exception as primary: + try: + self.close() + except Exception as cleanup: + raise OrchestrationError( + f"{primary}; startup cleanup failed: {cleanup}" + ) from primary + raise + + def close(self) -> None: + errors: list[str] = [] + death_proven = self.process is None + if self.process: + try: + self.process.stop(self.shutdown_timeout) + death_proven = self.process.death_proven + except Exception as error: # noqa: BLE001 - aggregate cleanup failures + errors.append(str(error)) + # A dead direct child does not prove that every member of its + # owned session exited. Preserve the data root on any stop error. + death_proven = False + if self.log_stream: + self.log_stream.close() + if self.temp and death_proven: + try: + self.temp.cleanup() + except Exception as error: # noqa: BLE001 - aggregate cleanup failures + errors.append(str(error)) + elif self.temp: + errors.append(f"retained live or unproven PostgreSQL data at {self.temp.path}") + if errors: + raise OrchestrationError("PostgreSQL cleanup failed: " + "; ".join(errors)) + + +def _database_url(admin_dsn: str, database: str) -> str: + from psycopg.conninfo import conninfo_to_dict, make_conninfo + + fields = conninfo_to_dict(admin_dsn) + fields["dbname"] = database + return make_conninfo(**fields) + + +class DatabaseOwner: + def __init__(self, admin_dsn: str, run_id: str, timeout: float) -> None: + self.admin_dsn = admin_dsn + self.run_id = run_id + self.timeout = timeout + self.created: list[str] = [] + + def _connect(self, dsn: str, *, autocommit: bool): + import psycopg + + milliseconds = max(1, math.ceil(self.timeout * 1_000)) + return psycopg.connect( + dsn, + autocommit=autocommit, + connect_timeout=max(1, math.ceil(self.timeout)), + options=f"-c statement_timeout={milliseconds} -c lock_timeout={milliseconds}", + ) + + def create(self, index: int) -> str: + from psycopg import sql + + name = f"adcp_story_{self.run_id}_{index}" + if name in self.created: + raise OrchestrationError(f"database name was already claimed: {name}") + # Retain the exact possible-effect name before sending CREATE. Cleanup + # can then address only this name even if transport failure obscures the result. + self.created.append(name) + with self._connect(self.admin_dsn, autocommit=True) as connection: + connection.execute( + sql.SQL( + "CREATE DATABASE {} TEMPLATE template0 ENCODING 'UTF8' " + "LC_COLLATE 'C' LC_CTYPE 'C'" + ).format(sql.Identifier(name)) + ) + database_dsn = _database_url(self.admin_dsn, name) + with self._connect(database_dsn, autocommit=False) as connection: + row = connection.execute("SELECT current_database()").fetchone() + if row != (name,): + raise OrchestrationError(f"created database identity mismatch: {row}") + return database_dsn + + def close(self) -> None: + from psycopg import sql + + failures: list[str] = [] + for name in reversed(self.created): + try: + with self._connect(self.admin_dsn, autocommit=True) as connection: + connection.execute( + sql.SQL("DROP DATABASE {} WITH (FORCE)").format(sql.Identifier(name)) + ) + except Exception as error: # noqa: BLE001 - retain every cleanup failure + failures.append(f"{name}: {error}") + if failures: + raise OrchestrationError("database cleanup failed: " + "; ".join(failures)) + + +def _server_command( + inputs: FixedInputs, + case: Case, + *, + port: int, + database_url: str | None, + ready_file: Path, +) -> list[str]: + common = [ + "--package-lock", + str(inputs.lock), + "--expected-version", + inputs.version, + "--expected-integrity", + inputs.integrity, + "--adcp-version", + inputs.adcp_version, + "--port", + str(port), + "--host", + "127.0.0.1", + "--ready-file", + str(ready_file), + ] + if case.server_kind == "controlled": + return [ + str(inputs.node), + str(HERE / "ts_controlled_reporting_server.cjs"), + "--storyboard-id", + case.storyboard_id, + *common, + ] + if database_url is None: + raise OrchestrationError(f"{case.storyboard_id} requires an isolated PostgreSQL database") + mode = "managed" if case.server_kind == "managed" else "billing" + return [ + str(inputs.node), + str(HERE / "ts_managed_reporting_server.cjs"), + "--mode", + mode, + "--pg-url", + database_url, + *common, + ] + + +def _server_environment(auth_token: str, startup_proof: str) -> dict[str, str]: + """Supply private inputs outside argv, without claiming same-user secrecy.""" + + return { + **_minimal_environment(), + STORYBOARD_PRIVATE_ENV["auth_token"]: auth_token, + STORYBOARD_PRIVATE_ENV["startup_proof"]: startup_proof, + } + + +def _storyboard_command( + inputs: FixedInputs, + case: Case, + *, + endpoint: str, +) -> list[str]: + return [ + str(inputs.node), + str(inputs.install / "node_modules/@adcp/sdk/bin/adcp.js"), + "storyboard", + "run", + endpoint, + case.storyboard_id, + "--json", + "--protocol", + "mcp", + "--allow-http", + "--sandbox", + "--timeout", + "180", + ] + + +def _step_rows(result: Mapping[str, Any]) -> list[Mapping[str, Any]]: + return [ + step + for track in result.get("tracks", []) + for scenario in track.get("scenarios", []) + for step in scenario.get("steps", []) + ] + + +def _wait_owned_server( + process: OwnedProcess, + ready_file: Path, + expected: Mapping[str, Any], + timeout: float, +) -> dict[str, Any]: + deadline = time.monotonic() + timeout + last_error: Exception | None = None + while time.monotonic() < deadline: + if process.process.poll() is not None: + raise OrchestrationError( + f"owned seller exited before readiness with {process.process.returncode}" + ) + if ready_file.is_file(): + try: + ready = _load_json(ready_file) + _validate_seller_readiness(ready, expected) + with socket.create_connection(("127.0.0.1", expected["port"]), timeout=0.2): + return dict(ready) + except (OSError, OrchestrationError) as error: + last_error = error + time.sleep(0.05) + raise OrchestrationError(f"owned seller readiness timed out: {last_error}") + + +def _validate_seller_readiness(ready: Mapping[str, Any], expected: Mapping[str, Any]) -> None: + """Bind a ready record to the seller process, package, runtime, and run secret.""" + + for field in ("pid", "port", "startup_proof_sha256", "auth_binding_sha256"): + if ready.get(field) != expected.get(field): + raise OrchestrationError(f"seller readiness {field} mismatch") + if ready.get("seller_package") != expected.get("seller_package"): + raise OrchestrationError("seller readiness package identity mismatch") + observed_node = ready.get("node") + expected_node = expected.get("node") + if not isinstance(observed_node, dict) or not isinstance(expected_node, dict): + raise OrchestrationError("seller readiness runtime identity missing") + if observed_node.get("executable") != expected_node.get("executable"): + raise OrchestrationError("seller readiness runtime executable mismatch") + version = observed_node.get("version") + if not isinstance(version, str) or not version.startswith("v"): + raise OrchestrationError("seller readiness runtime version missing") + + +def evaluate_case( + case: Case, + inventory_row: Mapping[str, Any], + result: Mapping[str, Any], + exit_code: int, +) -> dict[str, Any]: + if result.get("storyboards_executed") != [case.storyboard_id]: + raise OrchestrationError(f"storyboard selection drifted for {case.storyboard_id}") + expected = inventory_row["steps"] + observed = _step_rows(result) + expected_counts = Counter((item.get("title"), item.get("task")) for item in expected) + observed_counts = Counter((item.get("step"), item.get("task")) for item in observed) + buckets: dict[tuple[Any, Any], list[Mapping[str, Any]]] = {} + for step in observed: + buckets.setdefault((step.get("step"), step.get("task")), []).append(step) + executed: list[tuple[Mapping[str, Any], Mapping[str, Any]]] = [] + for identity in expected: + bucket = buckets.get((identity.get("title"), identity.get("task")), []) + if bucket: + executed.append((identity, bucket.pop(0))) + executed_ids = [identity["id"] for identity, _ in executed] + missing_ids = [identity["id"] for identity in expected if identity["id"] not in executed_ids] + unexpected_rows = [ + { + "step": step.get("step"), + "task": step.get("task"), + "passed": step.get("passed"), + "skipped": step.get("skipped"), + } + for key, count in (observed_counts - expected_counts).items() + for step in [item for item in observed if (item.get("step"), item.get("task")) == key][ + :count + ] + ] + passed = ( + exit_code == 0 + and not missing_ids + and not unexpected_rows + and observed_counts == expected_counts + and len(observed) == case.step_count + and len(executed) == case.step_count + and all( + step.get("passed") is True and step.get("skipped") is not True for _, step in executed + ) + ) + return { + "id": case.storyboard_id, + "exit_code": exit_code, + "resolved_steps": case.step_count, + "resolved_stateful_steps": case.stateful_step_count, + "executed_step_ids": executed_ids, + "unexecuted_step_ids": missing_ids, + "observed_result_row_count": len(observed), + "observed_identity_counts": [ + {"step": key[0], "task": key[1], "count": count} + for key, count in sorted(observed_counts.items(), key=lambda item: repr(item[0])) + ], + "unexpected_result_rows": unexpected_rows, + "fully_executed": passed, + "summary": result.get("summary"), + } + + +def _minimal_environment() -> dict[str, str]: + allowed = {"PATH", "LANG", "LC_ALL", "TZ", "TMPDIR", "SYSTEMROOT", "WINDIR"} + return {key: value for key, value in os.environ.items() if key in allowed} + + +def _storyboard_environment(auth_token: str) -> dict[str, str]: + """Supply the CLI bearer through its supported environment fallback.""" + + return { + **_minimal_environment(), + "ADCP_ALLOW_INTERNAL_PROBES": "1", + "ADCP_AUTH_TOKEN": auth_token, + } + + +def _run_owned_to_files( + command: Sequence[str], + *, + cwd: Path, + env: Mapping[str, str], + stdout_path: Path, + stderr_path: Path, + timeout: float, + shutdown_timeout: float, +) -> int: + """Run one command with owned descendants and retain output on every exit path.""" + + primary: BaseException | None = None + cleanup: BaseException | None = None + owned: OwnedProcess | None = None + with stdout_path.open("wb") as stdout, stderr_path.open("wb") as stderr: + try: + owned = OwnedProcess.spawn( + command, + ownership_timeout=shutdown_timeout, + cwd=cwd, + env=dict(env), + stdin=subprocess.DEVNULL, + stdout=stdout, + stderr=stderr, + start_new_session=True, + ) + owned.process.wait(timeout=timeout) + except subprocess.TimeoutExpired as error: + primary = OrchestrationError( + f"command timed out after {timeout}s; partial output retained at " + f"{stdout_path} and {stderr_path}" + ) + primary.__cause__ = error + except BaseException as error: # preserve interrupts while guaranteeing owned cleanup + primary = error + finally: + if owned is not None: + try: + owned.stop(shutdown_timeout) + except BaseException as error: + cleanup = error + if primary is not None and cleanup is not None: + raise OrchestrationError( + f"execution failed: {primary}; cleanup failed: {cleanup}" + ) from primary + if cleanup is not None: + raise OrchestrationError(f"command cleanup failed: {cleanup}") from cleanup + if primary is not None: + raise primary + if owned is None or owned.process.returncode is None: + raise OrchestrationError("owned command completed without a settled return code") + return owned.process.returncode + + +def _run_case( + inputs: FixedInputs, + case: Case, + inventory_row: Mapping[str, Any], + *, + index: int, + port: int, + database_url: str | None, + output: Path, + startup_timeout: float, + storyboard_timeout: float, + shutdown_timeout: float, +) -> dict[str, Any]: + case_output = output / case.storyboard_id + case_output.mkdir() + server_stdout_path = case_output / "server.stdout.log" + server_stderr_path = case_output / "server.stderr.log" + auth_token = secrets.token_urlsafe(32) + startup_proof = secrets.token_hex(32) + ready_file = case_output / "seller.ready.json" + server_command = _server_command( + inputs, + case, + port=port, + database_url=database_url, + ready_file=ready_file, + ) + primary: BaseException | None = None + cleanup: BaseException | None = None + result: dict[str, Any] | None = None + process: OwnedProcess | None = None + with server_stdout_path.open("wb") as stdout, server_stderr_path.open("wb") as stderr: + try: + process = OwnedProcess.spawn( + server_command, + ownership_timeout=shutdown_timeout, + cwd=ROOT, + env=_server_environment(auth_token, startup_proof), + stdin=subprocess.DEVNULL, + stdout=stdout, + stderr=stderr, + start_new_session=True, + ) + readiness = _wait_owned_server( + process, + ready_file, + { + "pid": process.pid, + "port": port, + "startup_proof_sha256": hashlib.sha256(startup_proof.encode()).hexdigest(), + "auth_binding_sha256": hashlib.sha256(auth_token.encode()).hexdigest(), + "seller_package": { + "name": "@adcp/sdk", + "version": inputs.version, + "integrity": inputs.integrity, + "adcp_version": inputs.adcp_version, + }, + "node": {"executable": str(inputs.node.resolve())}, + }, + startup_timeout, + ) + command = _storyboard_command( + inputs, + case, + endpoint=f"http://127.0.0.1:{port}/mcp", + ) + storyboard_stdout = case_output / "storyboard.stdout.json" + storyboard_stderr = case_output / "storyboard.stderr.log" + exit_code = _run_owned_to_files( + command, + cwd=case_output, + env=_storyboard_environment(auth_token), + timeout=storyboard_timeout, + shutdown_timeout=shutdown_timeout, + stdout_path=storyboard_stdout, + stderr_path=storyboard_stderr, + ) + try: + observed = json.loads(storyboard_stdout.read_text(encoding="utf-8")) + except json.JSONDecodeError as error: + raise OrchestrationError( + f"{case.storyboard_id} emitted invalid JSON with exit {exit_code}" + ) from error + result = evaluate_case(case, inventory_row, observed, exit_code) + result["seller_readiness"] = readiness + result["seller_process_identity"] = { + "pid": process.pid, + "start_token": process.identity.start_token, + "startup_proof_sha256": hashlib.sha256(startup_proof.encode()).hexdigest(), + } + result["seller_artifact_identity"] = { + "language": "typescript", + "package_role": inputs.role, + "package": "@adcp/sdk", + "version": inputs.version, + "integrity": inputs.integrity, + "adcp_version": inputs.adcp_version, + "tarball_sha256": inputs.tarball_sha256, + "package_member_manifest_sha256": inputs.package_member_manifest_sha256, + "node_sha256": inputs.node_sha256, + "artifact_identity_sha256": inputs.tarball_sha256, + "runtime_identity_sha256": _manifest_sha256( + {"node_path": str(inputs.node), "node_sha256": inputs.node_sha256} + ), + } + except BaseException as error: + primary = error + finally: + if process is not None: + try: + process.stop(shutdown_timeout) + except BaseException as error: + cleanup = error + if primary is not None and cleanup is not None: + raise OrchestrationError( + f"{case.storyboard_id} execution failed: {primary}; seller cleanup failed: {cleanup}" + ) from primary + if cleanup is not None: + raise OrchestrationError( + f"{case.storyboard_id} seller cleanup failed: {cleanup}" + ) from cleanup + if primary is not None: + raise primary + if result is None: + raise OrchestrationError(f"{case.storyboard_id} produced no result") + return result + + +def execute_case_sequence( + cases: Iterable[Case], + runner: Callable[[int, Case], dict[str, Any]], + cleanup: Callable[[], None], +) -> tuple[list[dict[str, Any]], list[str]]: + """Run in order, stop on the first failure, and always surface cleanup errors.""" + + rows: list[dict[str, Any]] = [] + errors: list[str] = [] + try: + for index, case in enumerate(cases): + try: + row = runner(index, case) + rows.append(row) + except Exception as error: # noqa: BLE001 - convert to retained result + errors.append(f"{case.storyboard_id}: {error}") + break + if row.get("fully_executed") is not True: + errors.append(f"{case.storyboard_id}: storyboard did not fully execute") + break + finally: + try: + cleanup() + except Exception as error: # noqa: BLE001 - cleanup failure is blocking + errors.append(f"cleanup: {error}") + return rows, errors + + +def _plan(inputs: FixedInputs, args: argparse.Namespace) -> dict[str, Any]: + return { + "schema_version": 1, + "mode": "execute" if args.execute else "validate_only", + "acceptance": False, + "blocking_acceptance": False, + "typescript": { + "role": inputs.role, + "version": inputs.version, + "integrity": inputs.integrity, + "registry_tarball": str(inputs.tarball), + "registry_tarball_sha256": inputs.tarball_sha256, + "package_lock": str(inputs.lock), + "package_lock_sha256": inputs.lock_sha256, + "package_member_count": inputs.package_member_count, + "package_member_manifest_sha256": inputs.package_member_manifest_sha256, + }, + "node": {"path": str(inputs.node), "sha256": inputs.node_sha256}, + "database_mode": "external" if args.external_pg_admin_dsn else "owned_local", + "cases": [case.__dict__ for case in inputs.cases], + "totals": EXPECTED_TOTALS, + "timeouts": { + "startup": args.startup_timeout, + "storyboard": args.storyboard_timeout, + "shutdown": args.shutdown_timeout, + }, + "limitations": [ + "storyboard_execution_is_not_six_cell_matrix_acceptance", + "probe_scheduler_dst_remains_mandatory_and_uncredited_until_it_executes", + "python_final_artifacts_and_1172_composition_are_not_selected_here", + "prior_cyberPolicy_reason_is_not_available_in_retained_tool_output", + ], + } + + +def execute(inputs: FixedInputs, args: argparse.Namespace) -> dict[str, Any]: + if args.output is None: + raise OrchestrationError("--output is required with --execute") + output = args.output.resolve() + output.mkdir(parents=True, exist_ok=False) + plan = _plan(inputs, args) + (output / "plan.json").write_text( + json.dumps(plan, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + local: LocalPostgres | None = None + owner: DatabaseOwner | None = None + rows: list[dict[str, Any]] = [] + errors: list[str] = [] + primary: BaseException | None = None + cleanup_errors: list[str] = [] + admin_dsn: str | None = None + inventory_by_id = {row["id"]: row for row in _inventory_rows(inputs.inventory)} + + def run(index: int, case: Case) -> dict[str, Any]: + if owner is None: + raise OrchestrationError("database owner was not initialized") + database_url = owner.create(index) if case.server_kind != "controlled" else None + return _run_case( + inputs, + case, + inventory_by_id[case.storyboard_id], + index=index, + port=args.service_port_base + index, + database_url=database_url, + output=output, + startup_timeout=args.startup_timeout, + storyboard_timeout=args.storyboard_timeout, + shutdown_timeout=args.shutdown_timeout, + ) + + try: + if args.external_pg_admin_dsn: + admin_dsn = args.external_pg_admin_dsn + else: + local = LocalPostgres( + pg_bin=args.local_pg_bin, + port=args.local_pg_port, + output=output, + startup_timeout=args.startup_timeout, + shutdown_timeout=args.shutdown_timeout, + ) + admin_dsn = local.start() + owner = DatabaseOwner(admin_dsn, secrets.token_hex(6), args.startup_timeout) + rows, errors = execute_case_sequence(inputs.cases, run, lambda: None) + except BaseException as error: + primary = error + finally: + if owner is not None: + try: + owner.close() + except BaseException as error: + cleanup_errors.append(f"database cleanup: {error}") + if local is not None: + try: + local.close() + except BaseException as error: + cleanup_errors.append(f"PostgreSQL cleanup: {error}") + if primary is not None and cleanup_errors: + raise OrchestrationError( + f"execution failed: {primary}; " + "; ".join(cleanup_errors) + ) from primary + if primary is not None: + raise primary + errors.extend(cleanup_errors) + if local is not None: + plan["owned_postgresql_identity"] = local.identity + completed = ( + len(rows) == len(inputs.cases) + and all(row.get("fully_executed") is True for row in rows) + and not errors + ) + result = { + **plan, + "status": "storyboard_complete_nonaccepting" if completed else "incomplete", + "execution_complete": completed, + "acceptance": False, + "blocking_acceptance": False, + "rows": rows, + "errors": errors, + } + (output / "results.json").write_text( + json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + return result + + +def main(argv: Sequence[str] | None = None) -> int: + args = parse_args(argv if argv is not None else sys.argv[1:]) + try: + inputs = validate_fixed_inputs(args) + if not args.execute: + print(json.dumps(_plan(inputs, args), indent=2, sort_keys=True)) + return 0 + result = execute(inputs, args) + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 if result["execution_complete"] else 1 + except Exception as error: # noqa: BLE001 - command boundary must fail closed + print( + json.dumps( + { + "status": "harness_error", + "acceptance": False, + "blocking_acceptance": False, + "error": str(error), + }, + sort_keys=True, + ), + file=sys.stderr, + ) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/reporting_interop/ts_adcp_version.cjs b/scripts/ci/reporting_interop/ts_adcp_version.cjs new file mode 100644 index 000000000..8b7f4defe --- /dev/null +++ b/scripts/ci/reporting_interop/ts_adcp_version.cjs @@ -0,0 +1,33 @@ +'use strict'; + +/* Convert an exact SDK release identifier to the protocol's major/minor wire + * precision. The release patch is never carried on the wire; any prerelease + * suffix remains attached to the major/minor version. Malformed or imprecise + * input is rejected rather than guessed. + */ +function wireAdcpVersion(value) { + const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(value); + if (!match) throw new Error(`AdCP version must use release precision: ${String(value)}`); + const [, major, minor, , prerelease = ''] = match; + if (prerelease.slice(1).split('.').some(identifier => /^0\d+$/.test(identifier))) { + throw new Error(`AdCP version must use release precision: ${String(value)}`); + } + return `${major}.${minor}${prerelease}`; +} + +/* This is deliberately an assertion for harness-owned fixture documents. It + * requires the fixture's singleton supported_versions declaration and must not + * be reused as a general validator for remote capability documents. + */ +function assertFixtureCapabilityVersion(capability, exactRelease) { + const wire = wireAdcpVersion(exactRelease); + if (capability?.adcp_version !== wire || + !Array.isArray(capability?.adcp?.supported_versions) || + capability.adcp.supported_versions.length !== 1 || + capability.adcp.supported_versions[0] !== wire) { + throw new Error('capability wire version does not match the selected exact SDK release'); + } + return wire; +} + +module.exports = { assertFixtureCapabilityVersion, wireAdcpVersion }; diff --git a/scripts/ci/reporting_interop/ts_controlled_reporting_fixture.cjs b/scripts/ci/reporting_interop/ts_controlled_reporting_fixture.cjs new file mode 100644 index 000000000..008fcb022 --- /dev/null +++ b/scripts/ci/reporting_interop/ts_controlled_reporting_fixture.cjs @@ -0,0 +1,560 @@ +'use strict'; + +/* + * Controlled-clock Reliable Reporting fixture for the installed rc.45 SDK. + * + * This implements the public ReportingConsumerStatusLedgerStore boundary and + * the read methods consumed by createReportingStatusHandler. It deliberately + * is not a PostgreSQL substitute: durable/store concurrency lanes continue to + * use PostgresReportingLedgerStore. Its only purpose is to drive storyboard + * instants that the PostgreSQL store (correctly) takes from the database clock. + */ + +const { createHash } = require('node:crypto'); + +const PERIOD_START = '2026-08-01T00:00:00.000Z'; +const PERIOD_END = '2026-08-01T01:00:00.000Z'; +const EXPECTED_AT = '2026-08-01T02:00:00.000Z'; +const RECOVERY_DEADLINE = '2026-08-01T04:00:00.000Z'; +const DELAYED_AT = '2026-08-01T02:05:00.000Z'; +const ACTION_REQUIRED_AT = '2026-08-01T04:05:00.000Z'; +const DIGEST = '5199a776b3a99915f084e16c921b2e501fcedd949017236d51a2303c5c2f5cd1'; + +function clone(value) { + return structuredClone(value); +} + +function sha(value) { + return createHash('sha256').update(JSON.stringify(value)).digest('hex'); +} + +function sourceRequest(source) { + return source.redactedReportingSourceRequestV1(); +} + +function configuration(accountId, source) { + const request = sourceRequest(source); + return { + configurationId: `configuration-${accountId}`, + account: { account_id: accountId }, + sourceScope: clone(request.sourceScope), + delivery_config_id: `delivery-${accountId}`, + delivery_config_version: 1, + offeringId: request.offeringId, + report_definition_id: request.report_definition_id, + feedPurpose: 'analytics', + requiredFinality: 'snapshot', + requestedMetrics: clone(request.requestedMetrics), + requestedDimensions: clone(request.requestedDimensions), + constituents: clone(request.coverage.constituents), + mediaBuyIds: clone(request.coverage.mediaBuyIds), + sourceTimezone: 'UTC', + schedule: { + anchor: PERIOD_START, + periodMilliseconds: 3_600_000, + deliverySlaMilliseconds: 3_600_000, + recoveryWindowMilliseconds: 7_200_000, + periodDuration: 'PT1H', + alignment: 'source_timezone', + periodTimezone: 'UTC', + deliverySlaDuration: 'PT1H', + }, + sourceSettings: clone(request.sourceSettings), + contract: clone(request.contract), + installedAt: PERIOD_START, + // Freeze this fixture generation to its single storyboard period. Later + // elapsed periods belong to a successor configuration in real sellers. + supersededAt: PERIOD_END, + semanticFingerprint: `sha256:${'a'.repeat(64)}`, + }; +} + +function obligation(accountId, source) { + const request = sourceRequest(source); + const config = configuration(accountId, source); + return { + reporting_obligation_id: `obligation-${accountId}`, + configurationId: config.configurationId, + account: { account_id: accountId }, + sourceScope: clone(request.sourceScope), + delivery_config_id: config.delivery_config_id, + delivery_config_version: config.delivery_config_version, + offeringId: config.offeringId, + report_definition_id: config.report_definition_id, + feedPurpose: 'analytics', + requiredFinality: 'snapshot', + periodOrdinal: 0, + period: { start: PERIOD_START, end: PERIOD_END, sourceTimezone: 'UTC' }, + schedule: clone(config.schedule), + scopeResolvedAt: PERIOD_END, + coverage: { + status: 'full', + evaluatedAt: PERIOD_END, + mediaBuyIds: clone(request.coverage.mediaBuyIds), + fullyCoveredMediaBuyIds: clone(request.coverage.mediaBuyIds), + partiallyCoveredMediaBuyIds: [], + unsupportedMediaBuyIds: [], + unknownMediaBuyIds: [], + }, + requestedMetrics: clone(request.requestedMetrics), + requestedDimensions: clone(request.requestedDimensions), + constituents: clone(request.coverage.constituents), + mediaBuyIds: clone(request.coverage.mediaBuyIds), + sourceSettings: clone(request.sourceSettings), + contract: clone(request.contract), + expectedAt: EXPECTED_AT, + recoveryDeadlineAt: RECOVERY_DEADLINE, + publicationOffsets: [], + nextAttemptAt: EXPECTED_AT, + attemptCount: 0, + state: 'pending', + semanticFingerprint: `sha256:${'b'.repeat(64)}`, + createdAt: PERIOD_END, + }; +} + +function exactRows() { + return [ + { + period_start: PERIOD_START, + period_end: PERIOD_END, + impressions: 2, + dimensions: { media_buy_id: 'media-buy-core-001', package_id: 'package-core-001', country: 'US' }, + metrics: { impressions: 2, clicks: 1 }, + }, + { + period_start: PERIOD_START, + period_end: PERIOD_END, + impressions: 3, + dimensions: { media_buy_id: 'media-buy-core-002', package_id: 'package-core-002', country: 'CA' }, + metrics: { impressions: 3, clicks: 0 }, + }, + ]; +} + +function revision(accountId, source, options = {}) { + const { + number = 1, + createdAt = ACTION_REQUIRED_AT, + supersedes, + id: fixedId, + rows = [], + controlTotals = [], + digest = DIGEST, + } = options; + const accountObligation = obligation(accountId, source); + const id = fixedId ?? (number === 1 + ? `revision-${accountId}-1` + : `revision-${accountId}-${number}`); + return { + reporting_revision_id: id, + reporting_obligation_id: `obligation-${accountId}`, + revisionNumber: number, + finality: 'snapshot', + kind: 'snapshot', + ...(supersedes ? { supersedes_reporting_revision_id: supersedes } : {}), + binding: { + algorithm: 'rfc8785_jcs_v1', + sha256: digest, + byteCount: Buffer.byteLength(JSON.stringify({ reporting_revision_id: id, row_count: rows.length, control_totals: controlTotals, reporting_rows: rows })), + rowCount: rows.length, + }, + rows: clone(rows), + observedAt: createdAt, + dataThrough: PERIOD_END, + sourceReadCutoffAt: createdAt, + createdAt, + manifest: {}, + sourcePublicationId: `publication-${id}`, + wireRevision: { + account_id: accountId, + reporting_revision_id: id, + revision_content_sha256: digest, + report_definition_id: accountObligation.report_definition_id, + report_definition_uri: accountObligation.contract.reportDefinitionUri, + report_definition_sha256: accountObligation.contract.reportDefinitionSha256, + reporting_profile: accountObligation.contract.reportingProfile, + schema_version: accountObligation.contract.schemaVersion, + schema_uri: accountObligation.contract.schemaUri, + schema_sha256: accountObligation.contract.schemaSha256, + schema_dialect: accountObligation.contract.schemaDialect, + schema_ref_policy: accountObligation.contract.schemaRefPolicy, + media_buy_ids: clone(accountObligation.mediaBuyIds), + coverage: { + status: accountObligation.coverage.status, + evaluated_at: accountObligation.coverage.evaluatedAt, + media_buy_ids: clone(accountObligation.coverage.mediaBuyIds), + fully_covered_media_buy_ids: clone(accountObligation.coverage.fullyCoveredMediaBuyIds), + partially_covered_media_buy_ids: [], + unsupported_media_buy_ids: [], + unknown_media_buy_ids: [], + package_ids: [], + covered_package_ids: [], + unsupported_package_ids: [], + unknown_package_ids: [], + limitations: [], + }, + period: { + start: accountObligation.period.start, + end: accountObligation.period.end, + source_timezone: accountObligation.period.sourceTimezone, + }, + finality: 'snapshot', + observed_at: createdAt, + data_through: PERIOD_END, + data_through_precision: 'exact', + ...(supersedes ? { supersedes_reporting_revision_id: supersedes } : {}), + row_count: rows.length, + control_totals: clone(controlTotals), + created_at: createdAt, + }, + }; +} + +class ControlledReportingStore { + constructor(api, accountId) { + this.api = api; + this.accountId = accountId; + this.ledgerAsOf = EXPECTED_AT; + this.configurations = [configuration(accountId, api.source)]; + this.obligations = [obligation(accountId, api.source)]; + this.revisions = []; + this.adjustments = []; + this.issues = []; + this.consumerStatements = []; + this.consumerStatusBatches = new Map(); + this.snapshots = new Map(); + } + + setClock(value) { + if (!Number.isFinite(Date.parse(value))) throw new TypeError('fixture clock must be an RFC 3339 instant'); + this.ledgerAsOf = new Date(value).toISOString(); + } + + async listConfigurations(accountId) { + return clone(this.configurations.filter(value => value.account.account_id === accountId)); + } + + async getObligation(id, accountId) { + return clone(this.obligations.find(value => + value.reporting_obligation_id === id && (!accountId || value.account.account_id === accountId)) ?? null); + } + + async getRevisionMetadata(id, accountId) { + const value = this.revisions.find(item => + item.reporting_revision_id === id && item.wireRevision.account_id === accountId); + return clone(value ?? null); + } + + async getRevision(id, accountId) { + return this.getRevisionMetadata(id, accountId); + } + + async listRevisionMetadata(obligationId, accountId) { + return clone(this.revisions.filter(value => + value.reporting_obligation_id === obligationId && value.wireRevision.account_id === accountId)); + } + + scopedStatuses(consumerId) { + if (!consumerId) return []; + return clone(this.consumerStatements.filter(value => + value.account_id === this.accountId && value.consumerId === consumerId)); + } + + chainKey(status) { + return [ + status.delivery_config_id, + status.delivery_config_version, + status.report_definition_id, + status.period.start, + status.period.end, + status.period.source_timezone, + ].join('\u0000'); + } + + currentLeaf(status, consumerId) { + const chain = this.consumerStatements.filter(value => + value.account_id === this.accountId && value.consumerId === consumerId && + this.chainKey(value) === this.chainKey(status)); + const superseded = new Set(chain.map(value => value.supersedes_reporting_status_id).filter(Boolean)); + return chain.find(value => !superseded.has(value.reporting_status_id)); + } + + async getConsumerStatusBatchReplay({ account_id, consumerId, idempotencyKey, requestFingerprint }) { + if (account_id !== this.accountId) return null; + const key = [account_id, consumerId, idempotencyKey].join('\u0000'); + const prior = this.consumerStatusBatches.get(key); + if (!prior) return null; + if (prior.requestFingerprint !== requestFingerprint) { + throw new this.api.ledger.ReportingConsumerStatusConflictError('idempotency key was reused'); + } + return clone(prior.results); + } + + async syncConsumerStatusBatch(input) { + const batchKey = [input.account_id, input.consumerId, input.idempotencyKey].join('\u0000'); + const prior = this.consumerStatusBatches.get(batchKey); + if (prior) { + if (prior.requestFingerprint !== input.requestFingerprint) { + throw new this.api.ledger.ReportingConsumerStatusConflictError('idempotency key was reused'); + } + return clone(prior.results); + } + const idCounts = new Map(); + const chainCounts = new Map(); + for (const entry of input.entries) { + const id = entry.status?.reporting_status_id ?? entry.reporting_status_id; + idCounts.set(id, (idCounts.get(id) ?? 0) + 1); + if (entry.status) { + const key = this.chainKey(entry.status); + chainCounts.set(key, (chainCounts.get(key) ?? 0) + 1); + } + } + const results = []; + for (const entry of input.entries) { + const id = entry.status?.reporting_status_id ?? entry.reporting_status_id; + const fail = (errorCode, safeMessage) => ({ + inserted: false, + reporting_status_id: id, + errorCode, + safeMessage, + ...(entry.validationField ? { errorField: entry.validationField } : {}), + ...(entry.validationKeyword ? { errorKeyword: entry.validationKeyword } : {}), + }); + if (entry.validationError) { + results.push(fail('VALIDATION_ERROR', entry.validationError)); + continue; + } + if (idCounts.get(id) > 1 || chainCounts.get(this.chainKey(entry.status)) > 1) { + results.push(fail('IDEMPOTENCY_CONFLICT', 'Reporting status batch names the same chain twice')); + continue; + } + const existing = this.consumerStatements.find(value => + value.reporting_status_id === id && value.consumerId === input.consumerId); + const candidate = { ...entry.status, recorded_at: this.ledgerAsOf }; + if (existing) { + const { recorded_at: _left, ...left } = existing; + const { recorded_at: _right, ...right } = candidate; + if (JSON.stringify(left) !== JSON.stringify(right)) { + results.push(fail('IDEMPOTENCY_CONFLICT', 'Reporting status ID was reused with different content')); + } else { + results.push({ inserted: false, value: clone(existing) }); + } + continue; + } + const leaf = this.currentLeaf(entry.status, input.consumerId); + if ((leaf?.reporting_status_id) !== entry.status.supersedes_reporting_status_id) { + results.push(fail('IDEMPOTENCY_CONFLICT', 'Reporting status did not name the current leaf')); + continue; + } + this.consumerStatements.push(clone(candidate)); + results.push({ inserted: true, value: clone(candidate) }); + } + this.consumerStatusBatches.set(batchKey, { requestFingerprint: input.requestFingerprint, results: clone(results) }); + return clone(results); + } + + async createSnapshot(query) { + if (query.account_id !== this.accountId) throw new Error('account unavailable'); + const snapshot = { + snapshotId: `snapshot-${this.accountId}-${this.snapshots.size}`, + ledgerAsOf: this.ledgerAsOf, + changesCheckpoint: this.ledgerAsOf, + queryFingerprint: sha(query), + query: clone(query), + configurations: clone(this.configurations), + coverageOrdinals: this.obligations.map(value => ({ + configurationId: value.configurationId, + periodOrdinal: value.periodOrdinal, + })), + obligations: clone(this.obligations), + revisions: clone(this.revisions), + adjustments: clone(this.adjustments), + consumerStatuses: this.scopedStatuses(query.consumer_id), + issues: clone(this.issues), + }; + this.snapshots.set(snapshot.snapshotId, snapshot); + return clone(snapshot); + } + + async readSnapshotPage(snapshotId, accountId, cursor, limit) { + const snapshot = this.snapshots.get(snapshotId); + if (!snapshot || accountId !== this.accountId) { + throw new this.api.ledger.ReportingLedgerSnapshotUnavailableError(); + } + const items = snapshot.obligations.flatMap(item => [ + ...(snapshot.query.view === 'revision' ? [] : [{ kind: 'obligation', value: item }]), + ...snapshot.revisions.filter(value => value.reporting_obligation_id === item.reporting_obligation_id) + .map(value => ({ kind: 'revision', value })), + ]); + const offset = cursor ? JSON.parse(Buffer.from(cursor, 'base64url')).offset : 0; + const selected = items.slice(offset, offset + limit); + const nextOffset = offset + selected.length; + return { + snapshot: clone(snapshot), + obligations: selected.filter(value => value.kind === 'obligation').map(value => clone(value.value)), + revisions: selected.filter(value => value.kind === 'revision').map(value => clone(value.value)), + adjustments: [], + consumerStatuses: clone(snapshot.consumerStatuses), + totalCount: items.length, + offset, + limit, + hasMore: nextOffset < items.length, + ...(nextOffset < items.length + ? { nextCursor: Buffer.from(JSON.stringify({ offset: nextOffset })).toString('base64url') } + : {}), + }; + } + + prepare() { + const value = this.obligations[0]; + return { + account_id: this.accountId, + delivery_config_id: value.delivery_config_id, + delivery_config_version: value.delivery_config_version, + expected_at: value.expectedAt, + recovery_deadline: value.recoveryDeadlineAt, + reporting_obligation_id: value.reporting_obligation_id, + period: clone(value.period), + resolved_configuration: { + delivery_config_id: value.delivery_config_id, + delivery_config_version: value.delivery_config_version, + report_definition_id: value.report_definition_id, + }, + }; + } + + advance(targetHealth) { + this.setClock(targetHealth === 'delayed' ? DELAYED_AT : ACTION_REQUIRED_AT); + return { target_health: targetHealth, ledger_as_of: this.ledgerAsOf }; + } + + publishZero() { + if (this.revisions.length === 0) this.revisions.push(revision(this.accountId, this.api.source)); + const value = this.revisions.at(-1); + return { + finality: value.finality, + reporting_obligation_id: value.reporting_obligation_id, + reporting_revision_id: value.reporting_revision_id, + revision_content_sha256: value.binding.sha256, + row_count: value.binding.rowCount, + }; + } + + publishNonempty() { + if (this.revisions.length === 0) { + const rows = exactRows(); + const id = 'reporting-revision.ecc62efa00946aa1e2788ad9'; + const controlTotals = [{ name: 'impressions', value: '5', value_type: 'integer', unit: 'impressions' }]; + const bytes = this.api.sdk.canonicalize({ + reporting_revision_id: id, + row_count: rows.length, + control_totals: controlTotals, + reporting_rows: rows, + }); + const digest = createHash('sha256').update(bytes).digest('hex'); + if (digest !== DIGEST) throw new Error(`literal reporting vector digest changed: ${digest}`); + this.revisions.push(revision(this.accountId, this.api.source, { id, rows, controlTotals, digest })); + } + const value = this.revisions.at(-1); + return { + finality: value.finality, + reporting_obligation_id: value.reporting_obligation_id, + reporting_revision_id: value.reporting_revision_id, + revision_content_sha256: value.binding.sha256, + row_count: value.binding.rowCount, + }; + } + + restate(withinGrace) { + if (this.revisions.length === 0) this.publishZero(); + if (this.revisions.length === 1) { + this.revisions.push(revision(this.accountId, this.api.source, { + number: 2, + createdAt: '2026-08-01T04:10:00.000Z', + supersedes: this.revisions[0].reporting_revision_id, + })); + } + this.setClock(withinGrace === false ? '2026-08-01T05:15:00.000Z' : '2026-08-01T04:30:00.000Z'); + return { + reporting_revision_id: this.revisions.at(-1).reporting_revision_id, + supersedes_reporting_revision_id: this.revisions[0].reporting_revision_id, + within_grace: withinGrace !== false, + ledger_as_of: this.ledgerAsOf, + }; + } +} + +function createControlledController(api) { + const stores = new Map(); + const allowed = new Set([ + 'reporting_core_lab', + 'reporting_core_nonempty_lab', + 'reporting_consumer_omission_lab', + 'reporting_consumer_revision_lab', + 'reporting_consumer_silence_lab', + 'reporting_consumer_content_lab', + 'reporting_consumer_mismatch_lab', + ]); + const getStore = accountId => { + if (!stores.has(accountId)) stores.set(accountId, new ControlledReportingStore(api, accountId)); + return stores.get(accountId); + }; + const factory = { + scenarios: ['reporting_core_lifecycle_probe'], + createStore(input) { + const accountId = input?.account?.account_id; + if (!allowed.has(accountId) || input?.account?.sandbox !== true) { + throw new api.server.TestControllerError('FORBIDDEN', 'Controller requires an allowed sandbox account'); + } + const store = getStore(accountId); + return { + async reportingCoreLifecycleProbe(params) { + switch (params.operation) { + case 'prepare': + return { success: true, simulated: store.prepare() }; + case 'advance_time': + return { success: true, simulated: store.advance(params.target_health) }; + case 'publish_zero_row': + return { success: true, simulated: store.publishZero() }; + case 'publish_nonempty': + return { success: true, simulated: store.publishNonempty() }; + case 'omit_obligation': { + const prepared = store.prepare(); + store.obligations = []; + return { success: true, simulated: { + account_id: accountId, + expected_reporting_obligation_id: prepared.reporting_obligation_id, + omitted_period: clone(prepared.period), + resolved_configuration: clone(prepared.resolved_configuration), + } }; + } + case 'restate_after_received': + return { success: true, simulated: store.restate(params.within_grace) }; + case 'advance_past_status_deadline': + store.setClock(ACTION_REQUIRED_AT); + return { success: true, simulated: { ledger_as_of: store.ledgerAsOf } }; + case 'advance_past_escalation': + store.setClock('2026-08-01T04:45:00.000Z'); + return { success: true, simulated: { ledger_as_of: store.ledgerAsOf } }; + default: + throw new api.server.TestControllerError('INVALID_PARAMS', `Unsupported operation: ${String(params.operation)}`); + } + }, + }; + }, + }; + return { factory, getStore, stores }; +} + +module.exports = { + ACTION_REQUIRED_AT, + ControlledReportingStore, + DELAYED_AT, + DIGEST, + EXPECTED_AT, + PERIOD_END, + PERIOD_START, + RECOVERY_DEADLINE, + createControlledController, +}; diff --git a/scripts/ci/reporting_interop/ts_controlled_reporting_http_probe.cjs b/scripts/ci/reporting_interop/ts_controlled_reporting_http_probe.cjs new file mode 100644 index 000000000..d020ec460 --- /dev/null +++ b/scripts/ci/reporting_interop/ts_controlled_reporting_http_probe.cjs @@ -0,0 +1,189 @@ +#!/usr/bin/env node +'use strict'; + +/* Real-MCP focused probe for the controlled reporting controller route. */ + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { createRequire } = require('node:module'); +const { callWithProbeAbort, createProbeAbort } = require('./ts_probe_abort.cjs'); + +function args(argv) { + const out = {}; + for (let i = 0; i < argv.length; i += 2) { + if (!argv[i]?.startsWith('--') || argv[i + 1] === undefined) throw new Error('invalid arguments'); + out[argv[i].slice(2)] = argv[i + 1]; + } + return out; +} + +async function main() { + const input = args(process.argv.slice(2)); + const lockPath = path.resolve(input['package-lock']); + const req = createRequire(path.join(path.dirname(lockPath), 'package.json')); + const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + const installed = req('@adcp/sdk/package.json'); + const entry = lock.packages?.['node_modules/@adcp/sdk']; + if (installed.version !== input['expected-version'] || entry?.integrity !== input['expected-integrity']) { + throw new Error('refusing to execute an unpinned SDK installation'); + } + const sdk = req('@adcp/sdk'); + const agent = { + agent_uri: input.endpoint, + auth_token: process.env.REPORTING_CONTROLLED_TOKEN, + id: 'controlled-reporting-fixture', + name: 'controlled-reporting-fixture', + protocol: 'mcp', + }; + if (!agent.auth_token) throw new Error('REPORTING_CONTROLLED_TOKEN is required'); + const probeAbort = createProbeAbort('controlled reporting HTTP probe'); + const call = async (toolName, params) => { + const raw = await callWithProbeAbort(probeAbort, signal => sdk.ProtocolClient.callTool( + agent, toolName, params, { + adcpVersion: input['adcp-version'], + signal, + wireAdcpVersion: input['adcp-version'], + transport: { requestTimeoutMs: 10_000, maxResponseBytes: 4 * 1024 * 1024 }, + }, + )); + return sdk.unwrapProtocolResponse(raw, toolName, 'mcp', { responseAdcpVersion: input['adcp-version'] }); + }; + const account = { account_id: 'reporting_core_lab' }; + try { + const prepared = await call('comply_test_controller', { + account: { ...account, sandbox: true }, + scenario: 'reporting_core_lifecycle_probe', + params: { operation: 'prepare' }, + }); + assert.equal(prepared.success, true); + await call('comply_test_controller', { + account: { ...account, sandbox: true }, + scenario: 'reporting_core_lifecycle_probe', + params: { operation: 'advance_time', target_health: 'delayed' }, + }); + const delayed = await call('get_reporting_status', { account, view: 'periods' }); + assert.equal(delayed.periods[0].health, 'delayed'); + const published = await call('comply_test_controller', { + account: { ...account, sandbox: true }, + scenario: 'reporting_core_lifecycle_probe', + params: { operation: 'publish_zero_row' }, + }); + assert.equal(published.simulated.row_count, 0); + const complete = await call('get_reporting_status', { account, view: 'periods' }); + assert.equal(complete.periods[0].health, 'complete'); + + const nonemptyAccount = { account_id: 'reporting_core_nonempty_lab' }; + await call('comply_test_controller', { + account: { ...nonemptyAccount, sandbox: true }, + scenario: 'reporting_core_lifecycle_probe', + params: { operation: 'prepare' }, + }); + const nonempty = await call('comply_test_controller', { + account: { ...nonemptyAccount, sandbox: true }, + scenario: 'reporting_core_lifecycle_probe', + params: { operation: 'publish_nonempty' }, + }); + assert.equal(nonempty.simulated.reporting_revision_id, 'reporting-revision.ecc62efa00946aa1e2788ad9'); + assert.equal(nonempty.simulated.revision_content_sha256, '5199a776b3a99915f084e16c921b2e501fcedd949017236d51a2303c5c2f5cd1'); + const pageOne = await call('get_media_buy_delivery', { + account: nonemptyAccount, + reporting_revision_id: nonempty.simulated.reporting_revision_id, + pagination: { max_results: 1 }, + }); + const pageTwo = await call('get_media_buy_delivery', { + account: nonemptyAccount, + reporting_revision_id: nonempty.simulated.reporting_revision_id, + pagination: { max_results: 1, cursor: pageOne.pagination.cursor }, + }); + assert.equal(pageOne.reporting_rows.length, 1); + assert.equal(pageOne.pagination.has_more, true); + assert.equal(pageTwo.reporting_rows.length, 1); + assert.equal(pageTwo.pagination.has_more, false); + + const consumerAccount = { account_id: 'reporting_consumer_revision_lab' }; + const consumerPrepared = await call('comply_test_controller', { + account: { ...consumerAccount, sandbox: true }, + scenario: 'reporting_core_lifecycle_probe', + params: { operation: 'prepare' }, + }); + await call('comply_test_controller', { + account: { ...consumerAccount, sandbox: true }, + scenario: 'reporting_core_lifecycle_probe', + params: { operation: 'advance_time', target_health: 'action_required' }, + }); + const preparedStatus = consumerPrepared.simulated; + const missing = { + reporting_status_id: 'consumer-status.http-revision-missing.0001', + delivery_config_id: preparedStatus.delivery_config_id, + delivery_config_version: preparedStatus.delivery_config_version, + report_definition_id: preparedStatus.resolved_configuration.report_definition_id, + period: { + start: preparedStatus.period.start, + end: preparedStatus.period.end, + source_timezone: preparedStatus.period.sourceTimezone, + }, + reporting_obligation_id: preparedStatus.reporting_obligation_id, + consumer_status: 'revision_missing', + status_as_of: '2026-08-01T04:05:00.000Z', + }; + const missingResult = await call('sync_reporting_status', { + account: consumerAccount, + idempotency_key: '00000000-0000-4000-8000-000000000011', + statuses: [missing], + }); + assert.equal(missingResult.results[0].result, 'recorded'); + const recovered = await call('comply_test_controller', { + account: { ...consumerAccount, sandbox: true }, + scenario: 'reporting_core_lifecycle_probe', + params: { operation: 'publish_zero_row' }, + }); + const receivedResult = await call('sync_reporting_status', { + account: consumerAccount, + idempotency_key: '00000000-0000-4000-8000-000000000012', + statuses: [{ + ...missing, + reporting_status_id: 'consumer-status.http-received.0001', + supersedes_reporting_status_id: missing.reporting_status_id, + reporting_revision_id: recovered.simulated.reporting_revision_id, + observed_revision_content_sha256: recovered.simulated.revision_content_sha256, + consumer_status: 'received', + }], + }); + assert.equal(receivedResult.results[0].result, 'recorded'); + const consumerComplete = await call('get_reporting_status', { account: consumerAccount, view: 'periods' }); + assert.equal(consumerComplete.periods[0].health, 'complete'); + assert.equal(consumerComplete.periods[0].consumer_status_count, 2); + const result = { + kind: 'controlled_reporting_controller_real_mcp_preflight', + status: 'passed', + blocking_acceptance: false, + package: { name: installed.name, version: installed.version }, + operations: { + prepare: prepared.simulated.reporting_obligation_id, + delayed: delayed.periods[0].health, + publish_zero_row: published.simulated.reporting_revision_id, + complete: complete.periods[0].health, + publish_nonempty: nonempty.simulated.reporting_revision_id, + exact_pages: [pageOne.reporting_rows.length, pageTwo.reporting_rows.length], + consumer_status: [missingResult.results[0].result, receivedResult.results[0].result], + consumer_complete: consumerComplete.periods[0].health, + }, + limitations: ['focused_real_mcp_preflight_not_cli_storyboard_execution', 'controlled_clock_fixture_not_postgresql'], + }; + fs.writeFileSync(input.output, `${JSON.stringify(result, null, 2)}\n`); + process.stdout.write(`${JSON.stringify(result)}\n`); + } finally { + probeAbort.abort('probe settled'); + try { + await sdk.closeMCPConnections(); + } finally { + probeAbort.dispose(); + } + } +} + +main().catch(error => { + process.stderr.write(`${JSON.stringify({ kind: 'controlled_reporting_http_probe_error', message: String(error?.message || error) })}\n`); + process.exitCode = 1; +}); diff --git a/scripts/ci/reporting_interop/ts_controlled_reporting_probe.cjs b/scripts/ci/reporting_interop/ts_controlled_reporting_probe.cjs new file mode 100644 index 000000000..644c45c9e --- /dev/null +++ b/scripts/ci/reporting_interop/ts_controlled_reporting_probe.cjs @@ -0,0 +1,271 @@ +#!/usr/bin/env node +'use strict'; + +/* Focused rc.45 public-handler probe for the controlled-clock controller. */ + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { createRequire } = require('node:module'); +const { createControlledController, DIGEST } = require('./ts_controlled_reporting_fixture.cjs'); + +function parseArgs(argv) { + const values = {}; + for (let index = 0; index < argv.length; index += 2) { + const flag = argv[index]; + const value = argv[index + 1]; + if (!flag?.startsWith('--') || value === undefined) throw new Error(`invalid argument near ${String(flag)}`); + values[flag.slice(2)] = value; + } + return values; +} + +function required(args, name) { + const value = args[name]; + if (!value) throw new Error(`missing --${name}`); + return value; +} + +function loadPinned(args) { + const lockPath = path.resolve(required(args, 'package-lock')); + const req = createRequire(path.join(path.dirname(lockPath), 'package.json')); + const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + const entry = lock.packages?.['node_modules/@adcp/sdk']; + const installed = req('@adcp/sdk/package.json'); + if (installed.version !== required(args, 'expected-version') || entry?.version !== installed.version || + entry?.integrity !== required(args, 'expected-integrity')) { + throw new Error('refusing to execute an unpinned SDK installation'); + } + const api = { + installed, + sdk: req('@adcp/sdk'), + ledger: req('@adcp/sdk/reporting/ledger'), + schemas: req('@adcp/sdk/schemas'), + server: req('@adcp/sdk/server'), + source: req('@adcp/sdk/reporting/source'), + }; + for (const [object, name] of [ + [api.ledger, 'createReportingStatusHandler'], + [api.ledger, 'createSyncReportingStatusHandler'], + [api.server, 'handleTestControllerRequest'], + ]) assert.equal(typeof object[name], 'function', `missing public export ${name}`); + return api; +} + +async function controller(api, factory, accountId, operation, extra = {}) { + const response = await api.server.handleTestControllerRequest(factory, { + account: { account_id: accountId, sandbox: true }, + scenario: 'reporting_core_lifecycle_probe', + params: { operation, ...extra }, + }); + const parsed = api.schemas.TOOL_RESPONSE_SCHEMAS.comply_test_controller.safeParse(response); + assert.equal(parsed.success, true, JSON.stringify(parsed.error?.issues)); + assert.equal(response.success, true, JSON.stringify(response)); + return response; +} + +function handlers(api, store, consumerId) { + const context = { account: { id: store.accountId }, consumer: consumerId }; + return { + context, + delivery: api.ledger.createReportingDeliveryHandler(store), + read: api.ledger.createReportingStatusHandler(store, { + resolveConsumerId: value => value.consumer, + consumerMismatchEscalation: { + escalationSeconds: 1_800, + operationsContact: { email: 'reporting-ops@example.test' }, + }, + }), + sync: api.ledger.createSyncReportingStatusHandler(store, { + resolveConsumerId: value => value.consumer, + now: () => new Date(store.ledgerAsOf), + }), + }; +} + +function validate(api, tool, response) { + const wire = { status: 'completed', ...api.server.toStructuredContent(response) }; + const parsed = api.schemas.TOOL_RESPONSE_SCHEMAS[tool].safeParse(wire); + assert.equal(parsed.success, true, `${tool}: ${JSON.stringify(parsed.error?.issues)}`); +} + +function statusInput(store, values) { + const prepared = store.prepare(); + return { + reporting_status_id: values.id, + ...(values.supersedes ? { supersedes_reporting_status_id: values.supersedes } : {}), + delivery_config_id: prepared.delivery_config_id, + delivery_config_version: prepared.delivery_config_version, + report_definition_id: prepared.resolved_configuration.report_definition_id, + period: { + start: prepared.period.start, + end: prepared.period.end, + source_timezone: prepared.period.sourceTimezone, + }, + reporting_obligation_id: prepared.reporting_obligation_id, + ...(values.revisionId ? { reporting_revision_id: values.revisionId } : {}), + ...(values.digest ? { observed_revision_content_sha256: values.digest } : {}), + consumer_status: values.consumerStatus, + status_as_of: store.ledgerAsOf, + }; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const api = loadPinned(args); + const fixture = createControlledController(api); + const operations = {}; + + const coreAccount = 'reporting_core_lab'; + operations.prepare = await controller(api, fixture.factory, coreAccount, 'prepare'); + const coreStore = fixture.getStore(coreAccount); + const core = handlers(api, coreStore, 'consumer-core'); + + await controller(api, fixture.factory, coreAccount, 'advance_time', { target_health: 'delayed' }); + const delayed = await core.read({ account: { account_id: coreAccount }, view: 'periods' }, core.context); + validate(api, 'get_reporting_status', delayed); + assert.equal(delayed.periods[0].health, 'delayed'); + operations.delayed = { ledger_as_of: delayed.ledger_as_of, health: delayed.periods[0].health }; + + await controller(api, fixture.factory, coreAccount, 'advance_time', { target_health: 'action_required' }); + const overdue = await core.read({ account: { account_id: coreAccount }, view: 'periods' }, core.context); + validate(api, 'get_reporting_status', overdue); + assert.equal(overdue.periods[0].health, 'action_required'); + operations.action_required = { ledger_as_of: overdue.ledger_as_of, health: overdue.periods[0].health }; + + const published = await controller(api, fixture.factory, coreAccount, 'publish_zero_row'); + const complete = await core.read({ account: { account_id: coreAccount }, view: 'periods' }, core.context); + validate(api, 'get_reporting_status', complete); + assert.equal(complete.periods[0].health, 'complete'); + operations.zero_row = { + revision: published.simulated.reporting_revision_id, + row_count: published.simulated.row_count, + health: complete.periods[0].health, + }; + + const nonemptyAccount = 'reporting_core_nonempty_lab'; + await controller(api, fixture.factory, nonemptyAccount, 'prepare'); + const nonempty = await controller(api, fixture.factory, nonemptyAccount, 'publish_nonempty'); + assert.equal(nonempty.simulated.reporting_revision_id, 'reporting-revision.ecc62efa00946aa1e2788ad9'); + assert.equal(nonempty.simulated.revision_content_sha256, DIGEST); + assert.equal(nonempty.simulated.row_count, 2); + const nonemptyStore = fixture.getStore(nonemptyAccount); + const nonemptyHandlers = handlers(api, nonemptyStore, 'consumer-core'); + const firstPage = await nonemptyHandlers.delivery({ + account: { account_id: nonemptyAccount }, + reporting_revision_id: nonempty.simulated.reporting_revision_id, + pagination: { max_results: 1 }, + }, nonemptyHandlers.context); + validate(api, 'get_media_buy_delivery', firstPage); + assert.equal(firstPage.reporting_rows.length, 1); + assert.equal(firstPage.pagination.has_more, true); + assert.equal(firstPage.reporting_revision_binding.content_sha256, DIGEST); + const secondPage = await nonemptyHandlers.delivery({ + account: { account_id: nonemptyAccount }, + reporting_revision_id: nonempty.simulated.reporting_revision_id, + pagination: { max_results: 1, cursor: firstPage.pagination.cursor }, + }, nonemptyHandlers.context); + validate(api, 'get_media_buy_delivery', secondPage); + assert.equal(secondPage.reporting_rows.length, 1); + assert.equal(secondPage.pagination.has_more, false); + assert.equal(secondPage.reporting_revision_binding.content_sha256, DIGEST); + operations.nonempty_exact_revision = { + revision: nonempty.simulated.reporting_revision_id, + digest: nonempty.simulated.revision_content_sha256, + rows: [...firstPage.reporting_rows, ...secondPage.reporting_rows], + }; + + const revisionAccount = 'reporting_consumer_revision_lab'; + await controller(api, fixture.factory, revisionAccount, 'prepare'); + await controller(api, fixture.factory, revisionAccount, 'advance_time', { target_health: 'action_required' }); + const revisionStore = fixture.getStore(revisionAccount); + const consumerId = 'https://buyer.example.test/adcp'; + const consumer = handlers(api, revisionStore, consumerId); + const missingStatus = statusInput(revisionStore, { + id: 'consumer-status.revision-missing.0001', + consumerStatus: 'revision_missing', + }); + const missing = await consumer.sync({ + account: { account_id: revisionAccount }, + idempotency_key: '00000000-0000-4000-8000-000000000001', + statuses: [missingStatus], + }, consumer.context); + validate(api, 'sync_reporting_status', missing); + assert.equal(missing.results[0].result, 'recorded'); + + const revisionResult = await controller(api, fixture.factory, revisionAccount, 'publish_zero_row'); + const revisionId = revisionResult.simulated.reporting_revision_id; + const mismatched = await consumer.read({ account: { account_id: revisionAccount }, view: 'periods' }, consumer.context); + validate(api, 'get_reporting_status', mismatched); + assert.equal(mismatched.periods[0].health, 'action_required'); + assert.ok(mismatched.periods[0].issues.some(issue => issue.code === 'CONSUMER_STATUS_MISMATCH')); + + const wrongBinding = await consumer.sync({ + account: { account_id: revisionAccount }, + idempotency_key: '00000000-0000-4000-8000-000000000002', + statuses: [statusInput(revisionStore, { + id: 'consumer-status.revision-wrong-binding.0001', + consumerStatus: 'received', + revisionId, + digest: '0'.repeat(64), + supersedes: missingStatus.reporting_status_id, + })], + }, consumer.context); + validate(api, 'sync_reporting_status', wrongBinding); + assert.equal(wrongBinding.results[0].result, 'failed'); + + const receivedRequest = { + account: { account_id: revisionAccount }, + idempotency_key: '00000000-0000-4000-8000-000000000003', + statuses: [statusInput(revisionStore, { + id: 'consumer-status.revision-received.0001', + consumerStatus: 'received', + revisionId, + digest: DIGEST, + supersedes: missingStatus.reporting_status_id, + })], + }; + const received = await consumer.sync(receivedRequest, consumer.context); + validate(api, 'sync_reporting_status', received); + assert.equal(received.results[0].result, 'recorded'); + const replay = await consumer.sync(receivedRequest, consumer.context); + assert.deepEqual(replay, received, 'idempotent replay must return the retained ordered result'); + + const resolved = await consumer.read({ account: { account_id: revisionAccount }, view: 'periods' }, consumer.context); + validate(api, 'get_reporting_status', resolved); + assert.equal(resolved.periods[0].health, 'complete'); + assert.equal(resolved.periods[0].consumer_status_count, 2); + + const other = handlers(api, revisionStore, 'opaque-other-consumer'); + const isolated = await other.read({ account: { account_id: revisionAccount }, view: 'periods' }, other.context); + validate(api, 'get_reporting_status', isolated); + assert.equal(isolated.periods[0].consumer_status_count, 0); + operations.consumer_status = { + missing: missing.results[0].result, + wrong_binding: wrongBinding.results[0].result, + received: received.results[0].result, + retained_count: resolved.periods[0].consumer_status_count, + other_consumer_count: isolated.periods[0].consumer_status_count, + }; + + const result = { + kind: 'controlled_clock_reporting_controller_preflight', + status: 'passed', + blocking_acceptance: false, + package: { name: api.installed.name, version: api.installed.version }, + operations, + limitations: [ + 'focused_public_handler_preflight_not_storyboard_execution', + 'controlled_clock_fixture_not_postgresql_durability_evidence', + 'probe_scheduler_dst_unavailable_in_stock_rc45_producer', + 'managed_delivery_and_reconciled_billing_not_exercised', + ], + }; + fs.writeFileSync(required(args, 'output'), `${JSON.stringify(result, null, 2)}\n`); + process.stdout.write(`${JSON.stringify(result)}\n`); +} + +main().catch(error => { + process.stderr.write(`${JSON.stringify({ kind: 'controlled_clock_probe_error', message: String(error?.message || error), stack: String(error?.stack || '').split('\n').slice(0, 5) })}\n`); + process.exitCode = 1; +}); diff --git a/scripts/ci/reporting_interop/ts_controlled_reporting_server.cjs b/scripts/ci/reporting_interop/ts_controlled_reporting_server.cjs new file mode 100644 index 000000000..60f7ad63b --- /dev/null +++ b/scripts/ci/reporting_interop/ts_controlled_reporting_server.cjs @@ -0,0 +1,362 @@ +#!/usr/bin/env node +'use strict'; + +/* + * Installed-package MCP server for controlled-clock Core/consumer-status + * storyboard operations. Durable PostgreSQL lanes use ts_core_server.cjs; + * this server exists only for fixed protocol-clock narratives. + */ + +const assert = require('node:assert/strict'); +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); +const { createRequire } = require('node:module'); +const { createControlledController } = require('./ts_controlled_reporting_fixture.cjs'); +const { assertFixtureCapabilityVersion, wireAdcpVersion } = require('./ts_adcp_version.cjs'); +const { storyboardPrivateInputs } = require('./ts_private_inputs.cjs'); + +const LAB_ACCOUNTS = new Set([ + 'reporting_core_lab', + 'reporting_core_nonempty_lab', + 'reporting_consumer_omission_lab', + 'reporting_consumer_revision_lab', + 'reporting_consumer_silence_lab', + 'reporting_consumer_content_lab', + 'reporting_consumer_mismatch_lab', +]); +const STORYBOARD_OPERATOR = 'test.example'; +const CONTROLLED_STORYBOARDS = new Set([ + 'reporting_core', + 'reporting_consumer_status', + 'reporting_core_declaration', +]); + +function parseArgs(argv) { + const values = { host: '127.0.0.1', probe: false }; + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (token === '--probe') { + values.probe = true; + continue; + } + if (!token.startsWith('--')) throw new Error(`unexpected positional argument: ${token}`); + const value = argv[index + 1]; + if (!value || value.startsWith('--')) throw new Error(`missing value for ${token}`); + values[token.slice(2)] = value; + index += 1; + } + return values; +} + +function required(args, name) { + if (!args[name]) throw new Error(`missing --${name}`); + return args[name]; +} + +function loadPinned(args) { + const lockPath = path.resolve(required(args, 'package-lock')); + const req = createRequire(path.join(path.dirname(lockPath), 'package.json')); + const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + const entry = lock.packages?.['node_modules/@adcp/sdk']; + const installed = req('@adcp/sdk/package.json'); + if (installed.version !== required(args, 'expected-version') || entry?.version !== installed.version || + entry?.integrity !== required(args, 'expected-integrity')) { + throw new Error('refusing to execute an unpinned SDK installation'); + } + const api = { + expectedIntegrity: required(args, 'expected-integrity'), + installed, + sdk: req('@adcp/sdk'), + ledger: req('@adcp/sdk/reporting/ledger'), + schemas: req('@adcp/sdk/schemas'), + server: req('@adcp/sdk/server'), + source: req('@adcp/sdk/reporting/source'), + z: req('zod').z, + }; + for (const [object, name] of [ + [api.ledger, 'createReportingDeliveryHandler'], + [api.ledger, 'createReportingStatusHandler'], + [api.ledger, 'createSyncReportingStatusHandler'], + [api.server, 'createTaskCapableServer'], + [api.server, 'handleTestControllerRequest'], + [api.server, 'legacyCapabilitiesResponse'], + [api.server, 'serve'], + [api.server, 'toMcpResponse'], + [api.server, 'verifyApiKey'], + ]) assert.equal(typeof object[name], 'function', `missing public export ${name}`); + assert.ok(api.server.TOOL_INPUT_SHAPE, 'missing public controller input shape'); + return api; +} + +function capabilities(adcpVersion) { + const wire = wireAdcpVersion(adcpVersion); + return { + adcp_version: wire, + adcp: { major_versions: [3], supported_versions: [wire], idempotency: { supported: false } }, + supported_protocols: ['media_buy'], + experimental_features: ['media_buy.reporting_delivery'], + compliance_testing: { scenarios: ['reporting_core_lifecycle_probe'] }, + account: { + require_operator_auth: true, + supported_billing: ['operator'], + supported_account_currency_modes: ['fixed'], + timezone: { mode: 'seller_fixed', fixed_timezone: 'UTC' }, + required_for_products: true, + sandbox: true, + }, + media_buy: { + reporting_delivery: { + supported: true, + reliable_reporting_version: '1.0', + managed_delivery: false, + reconciled_billing: false, + configuration_task: 'sync_accounts', + status_task: 'get_reporting_status', + consumer_status_task: 'sync_reporting_status', + consumer_mismatch_escalation_seconds: 1_800, + operations_contact: { email: 'reporting-ops@example.test' }, + revision_content_task: 'get_media_buy_delivery', + offerings: [{ + offering_id: 'controlled-core-hourly', + feed_purpose: 'analytics', + report_definition_id: 'delivery-daily-v1', + report_definition_uri: 'https://example.com/report-definition.json', + report_definition_sha256: 'a'.repeat(64), + reporting_profile: { + id: 'controlled-core-hourly', + version: '1', + schema_uri: 'https://example.com/reporting-schema.json', + schema_sha256: 'b'.repeat(64), + schema_dialect: 'https://json-schema.org/draft/2020-12/schema', + schema_ref_policy: 'local_fragment_only', + grain: 'media_buy', + primary_keys: ['media_buy_id'], + }, + schedule: { + period_duration: 'PT1H', + alignment: 'source_timezone', + period_timezone_policy: 'fixed', + period_timezone: 'UTC', + delivery_sla: 'PT1H', + }, + supported_finality: ['snapshot'], + reconciliation_mode: 'delivery_only', + }], + automated_recovery_window_seconds: 7_200, + status_retention_days: 30, + }, + }, + }; +} + +function mcpSuccess(api, data, summary) { + return { + content: [{ type: 'text', text: summary }], + structuredContent: { status: 'completed', ...api.server.toStructuredContent(data) }, + }; +} + +function createAccountRouter(storyboardId) { + if (!CONTROLLED_STORYBOARDS.has(storyboardId)) { + throw new Error(`unsupported controlled storyboard: ${storyboardId}`); + } + const consumerAccounts = [ + 'reporting_consumer_omission_lab', + 'reporting_consumer_revision_lab', + 'reporting_consumer_silence_lab', + 'reporting_consumer_content_lab', + ]; + let activeAccount = storyboardId === 'reporting_core' ? 'reporting_core_lab' : undefined; + let corePrepareCount = 0; + let consumerPrepareCount = 0; + return { + resolve(request, { controller = false } = {}) { + const exact = request?.account?.account_id; + if (typeof exact === 'string' && LAB_ACCOUNTS.has(exact)) { + activeAccount = exact; + return exact; + } + if (request?.account?.operator !== STORYBOARD_OPERATOR || request?.account?.sandbox !== true) { + throw new Error('request does not select the trusted storyboard sandbox operator'); + } + if (controller && request?.params?.operation === 'prepare') { + if (storyboardId === 'reporting_consumer_status') { + if (consumerPrepareCount >= consumerAccounts.length) { + throw new Error('consumer-status controller prepare sequence exceeded its declared accounts'); + } + activeAccount = consumerAccounts[consumerPrepareCount++]; + } else if (storyboardId === 'reporting_core') { + activeAccount = corePrepareCount++ === 0 + ? 'reporting_core_lab' + : 'reporting_core_nonempty_lab'; + } + } + if (!activeAccount) { + throw new Error('storyboard account is unavailable before its prepare operation'); + } + return activeAccount; + }, + }; +} + +function accountIdFor(request, extra, router, options) { + const allowed = extra?.authInfo?.extra?.accounts; + const accountId = router.resolve(request, options); + if (!Array.isArray(allowed) || !allowed.includes(accountId) || !LAB_ACCOUNTS.has(accountId)) { + throw new Error('caller is not authorized for this sandbox reporting account'); + } + return accountId; +} + +function validateAdvertisedCapabilities(api, adcpVersion, advertised = capabilities(adcpVersion)) { + assertFixtureCapabilityVersion(advertised, adcpVersion); + const validator = api.schemas.getCanonicalToolValidator( + 'get_adcp_capabilities', 'sync', { adcpVersion }, + ); + assert.equal(typeof validator, 'function', 'canonical capability validator is unavailable'); + assert.equal(Boolean(validator({ ...advertised, status: 'completed' })), true, + JSON.stringify(validator.errors || [])); + return advertised; +} + +function buildReadyRecord(api, { adcpVersion, authToken, port, startupProof, url }) { + return { + auth_binding_sha256: crypto.createHash('sha256').update(authToken).digest('hex'), + kind: 'controlled_reporting_storyboard_server_ready', + node: { executable: fs.realpathSync(process.execPath), version: process.version }, + seller_package: { + adcp_version: adcpVersion, + integrity: api.expectedIntegrity, + name: api.installed.name, + version: api.installed.version, + }, + startup_proof_sha256: crypto.createHash('sha256').update(startupProof).digest('hex'), + pid: process.pid, + port, + url, + }; +} + +function createAgent(api, fixture, advertised, storyboardId) { + const router = createAccountRouter(storyboardId); + return () => { + const mcp = api.server.createTaskCapableServer('Controlled reporting storyboard fixture', '1.0.0'); + mcp.registerTool('get_adcp_capabilities', { + description: 'Return controlled reporting fixture capabilities.', + inputSchema: api.schemas.TOOL_INPUT_SHAPES.get_adcp_capabilities, + }, async () => api.server.legacyCapabilitiesResponse(advertised)); + mcp.registerTool('comply_test_controller', { + description: 'Drive fixed-clock reporting state in declared sandbox accounts.', + inputSchema: api.server.TOOL_INPUT_SHAPE, + }, async (input, extra) => { + const accountId = accountIdFor(input, extra, router, { controller: true }); + const response = await api.server.handleTestControllerRequest(fixture.factory, { + ...input, + account: { account_id: accountId, sandbox: true }, + }); + return api.server.toMcpResponse(response); + }); + for (const tool of ['get_reporting_status', 'get_media_buy_delivery', 'sync_reporting_status']) { + mcp.registerTool(tool, { + description: `Controlled reporting fixture ${tool}.`, + inputSchema: api.schemas.TOOL_INPUT_SHAPES[tool], + }, async (request, extra) => { + const accountId = accountIdFor(request, extra, router); + const store = fixture.getStore(accountId); + const consumer = extra.authInfo.extra.consumer_id; + const context = { account: { account_id: accountId }, consumer, authInfo: extra.authInfo }; + const options = { resolveConsumerId: value => value.consumer }; + const result = tool === 'get_reporting_status' + ? await api.ledger.createReportingStatusHandler(store, { + ...options, + consumerMismatchEscalation: { + escalationSeconds: 1_800, + operationsContact: { email: 'reporting-ops@example.test' }, + }, + })(request, context) + : tool === 'get_media_buy_delivery' + ? await api.ledger.createReportingDeliveryHandler(store)(request, context) + : await api.ledger.createSyncReportingStatusHandler(store, { + ...options, + now: () => new Date(store.ledgerAsOf), + })(request, context); + return mcpSuccess(api, result, `${tool} completed`); + }); + } + return mcp; + }; +} + +async function main(argv = process.argv.slice(2), dependencies = {}) { + const args = parseArgs(argv); + const api = (dependencies.loadPinned || loadPinned)(args); + const adcpVersion = required(args, 'adcp-version'); + const storyboardId = args['storyboard-id'] || 'reporting_core'; + if (!CONTROLLED_STORYBOARDS.has(storyboardId)) { + throw new Error(`invalid --storyboard-id: ${storyboardId}`); + } + const advertised = validateAdvertisedCapabilities(api, adcpVersion); + let normalInputs; + if (!args.probe) { + const port = Number(required(args, 'port')); + if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) throw new Error('invalid --port'); + normalInputs = { + port, + ...storyboardPrivateInputs(dependencies.environment || process.env), + readyFile: path.resolve(required(args, 'ready-file')), + }; + } + const fixture = (dependencies.createControlledController || createControlledController)(api); + if (args.probe) { + (dependencies.writeOutput || (value => process.stdout.write(value)))(`${JSON.stringify({ + kind: 'controlled_reporting_storyboard_server_probe', + status: 'passed', + blocking_acceptance: false, + package: { name: api.installed.name, version: api.installed.version }, + scenarios: fixture.factory.scenarios, + operations: ['prepare', 'advance_time', 'publish_zero_row', 'omit_obligation', + 'publish_nonempty', 'restate_after_received', 'advance_past_status_deadline', 'advance_past_escalation'], + limitations: ['controlled_clock_fixture_not_postgresql', 'probe_scheduler_dst_unavailable'], + })}\n`); + return; + } + const { authToken, port, readyFile, startupProof } = normalInputs; + const authenticate = api.server.verifyApiKey({ + verify(token) { + if (token !== authToken) return null; + return { + principal: 'reporting-controlled-fixture-operator', + extra: { accounts: [...LAB_ACCOUNTS], consumer_id: 'https://buyer.example.test/adcp' }, + }; + }, + }); + const server = api.server.serve(createAgent(api, fixture, advertised, storyboardId), { + allowedHosts: [args.host, 'localhost', '127.0.0.1'], + authenticate, + path: '/mcp', + port, + onListening(url) { + const ready = buildReadyRecord(api, { adcpVersion, authToken, port, startupProof, url }); + fs.writeFileSync(readyFile, `${JSON.stringify(ready)}\n`, { flag: 'wx', mode: 0o600 }); + process.stdout.write(`${JSON.stringify(ready)}\n`); + }, + }); + let closing = false; + const close = () => new Promise(resolve => { + if (closing) return resolve(); + closing = true; + server.close(resolve); + }); + process.once('SIGINT', () => close().then(() => process.exit(0))); + process.once('SIGTERM', () => close().then(() => process.exit(0))); +} + +if (require.main === module) { + main().catch(error => { + process.stderr.write(`${JSON.stringify({ kind: 'controlled_reporting_server_error', message: String(error?.message || error) })}\n`); + process.exitCode = 1; + }); +} + +module.exports = { buildReadyRecord, capabilities, main, validateAdvertisedCapabilities }; diff --git a/scripts/ci/reporting_interop/ts_core_buyer.cjs b/scripts/ci/reporting_interop/ts_core_buyer.cjs new file mode 100644 index 000000000..98b476a72 --- /dev/null +++ b/scripts/ci/reporting_interop/ts_core_buyer.cjs @@ -0,0 +1,294 @@ +#!/usr/bin/env node +'use strict'; + +/* Public Core-only TypeScript buyer probe. + * + * Historical rc.42 deliberately failed this probe at the public-export gate. + * The exact selected candidate must exercise reconcileReportingCoreV1 over + * real MCP HTTP; older supplemental pins cannot promote it. + */ + +const fs = require('node:fs'); +const path = require('node:path'); +const { createRequire } = require('node:module'); +const { callWithProbeAbort, createProbeAbort } = require('./ts_probe_abort.cjs'); + +function parseArgs(argv) { + const values = { 'expect-missing-core-api': false }; + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (token === '--expect-missing-core-api') { + values['expect-missing-core-api'] = true; + continue; + } + if (!token.startsWith('--')) throw new Error(`unexpected positional argument: ${token}`); + const key = token.slice(2); + const value = argv[index + 1]; + if (!value || value.startsWith('--')) throw new Error(`missing value for --${key}`); + if (Object.hasOwn(values, key)) throw new Error(`duplicate argument: --${key}`); + values[key] = value; + index += 1; + } + return values; +} + +function requireArg(args, name) { + if (!args[name]) throw new Error(`missing required argument: --${name}`); + return args[name]; +} + +function stable(value) { + if (Array.isArray(value)) return value.map(stable); + if (value && typeof value === 'object') { + return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])])); + } + return value; +} + +function validateEndpoint(endpoint) { + const parsed = new URL(endpoint); + if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error('endpoint must use HTTP or HTTPS'); + if (parsed.username || parsed.password || parsed.search || parsed.hash) { + throw new Error('endpoint must not contain credentials, a query, or a fragment'); + } + return parsed.toString(); +} + +function packageContext(args) { + const packageName = args.package || '@adcp/sdk'; + const expectedVersion = requireArg(args, 'expected-version'); + const expectedIntegrity = requireArg(args, 'expected-integrity'); + const lockPath = path.resolve(requireArg(args, 'package-lock')); + const packageRequire = createRequire(path.join(path.dirname(lockPath), 'package.json')); + const installed = packageRequire(`${packageName}/package.json`); + const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + const entry = lock.packages?.[`node_modules/${packageName}`]; + if (installed.version !== expectedVersion || entry?.version !== expectedVersion || + entry?.integrity !== expectedIntegrity) { + throw new Error('refusing to execute an unpinned SDK installation'); + } + return { expectedIntegrity, installed, packageName, packageRequire }; +} + +function installRedactedConsole(secret) { + for (const level of ['log', 'warn', 'error', 'info', 'debug']) { + console[level] = (...items) => { + let line = items.map(item => typeof item === 'string' ? item : JSON.stringify(item)).join(' '); + if (secret) line = line.split(secret).join('[REDACTED]'); + process.stderr.write(`${line}\n`); + }; + } +} + +async function run() { + const args = parseArgs(process.argv.slice(2)); + const context = packageContext(args); + const sdk = context.packageRequire(context.packageName); + const expectMissing = Boolean(args['expect-missing-core-api']); + const hasCoreApi = typeof sdk.reconcileReportingCoreV1 === 'function'; + let coreSubpath = 'available'; + try { + context.packageRequire.resolve(`${context.packageName}/client/core`); + } catch (error) { + coreSubpath = error.code || error.name; + } + + if (!hasCoreApi && !args.endpoint) { + const result = { + core_client_subpath: coreSubpath, + core_reconciler_export: typeof sdk.reconcileReportingCoreV1, + kind: 'reporting_interop_ts_core_buyer', + package: { + integrity: context.expectedIntegrity, + name: context.packageName, + version: context.installed.version, + }, + schema_version: 1, + semantic_lane_complete: false, + status: 'unsupported', + }; + if (!expectMissing) process.exitCode = 1; + return result; + } + if (hasCoreApi && expectMissing) throw new Error('Core API unexpectedly exists in expected-gap mode'); + for (const name of ['ProtocolClient', 'unwrapProtocolResponse', 'closeMCPConnections']) { + if (typeof sdk[name] !== 'function') throw new Error(`missing public transport export: ${name}`); + } + + const endpoint = validateEndpoint(requireArg(args, 'endpoint')); + const adcpVersion = requireArg(args, 'adcp-version'); + const accountId = requireArg(args, 'account'); + const authEnv = requireArg(args, 'auth-env'); + const authToken = process.env[authEnv]; + if (!authToken) throw new Error(`authentication environment variable is unset: ${authEnv}`); + installRedactedConsole(authToken); + const agent = { + agent_uri: endpoint, + auth_token: authToken, + id: 'reporting-interop-core-seller', + name: 'reporting-interop-core-seller', + protocol: 'mcp', + }; + const probeAbort = createProbeAbort('reporting Core buyer probe'); + const call = async (toolName, params) => { + const raw = await callWithProbeAbort(probeAbort, signal => sdk.ProtocolClient.callTool( + agent, toolName, params, { + adcpVersion, + signal, + transport: { maxResponseBytes: 16 * 1024 * 1024, requestTimeoutMs: 30_000 }, + wireAdcpVersion: adcpVersion, + }, + )); + return sdk.unwrapProtocolResponse(raw, toolName, 'mcp', { + responseAdcpVersion: adcpVersion, + }); + }; + + let capability; + let page; + try { + capability = await call('get_adcp_capabilities', {}); + page = await call('get_reporting_status', { + account: { account_id: accountId }, + period: { start: '2026-08-01T00:00:00Z', end: '2026-08-02T00:00:00Z' }, + view: 'periods', + }); + } finally { + probeAbort.abort('probe settled'); + try { + await sdk.closeMCPConnections(); + } finally { + probeAbort.dispose(); + } + } + const rawFacts = { + account_id: page.account_id, + ledger_as_of: page.ledger_as_of, + obligation_count: page.periods.length, + revision_count: page.revisions.length, + }; + if (!hasCoreApi) { + if (!expectMissing) process.exitCode = 1; + return { + core_client_subpath: coreSubpath, + core_reconciler_export: typeof sdk.reconcileReportingCoreV1, + http_exercised: true, + kind: 'reporting_interop_ts_core_buyer', + package: { + integrity: context.expectedIntegrity, + name: context.packageName, + version: context.installed.version, + }, + raw_facts: rawFacts, + runtime: { node: process.version }, + schema_version: 1, + semantic_lane_complete: false, + status: 'unsupported', + }; + } + const recoveryWindow = capability?.media_buy?.reporting_delivery?.automated_recovery_window_seconds; + if (!Number.isSafeInteger(recoveryWindow) || recoveryWindow < 0) { + throw new Error('seller did not advertise a valid Core automated recovery window'); + } + const coreInput = { + clocks: { + automatedRecoveryWindowSeconds: recoveryWindow, + ledgerAsOf: page.ledger_as_of, + }, + obligations: page.periods, + revisions: page.revisions, + scope: { + closed: page.scope.scope_closed, + coverageComplete: page.scope.coverage_complete, + recordsComplete: page.pagination.has_more === false, + }, + }; + const inputBefore = JSON.stringify(coreInput); + const nativeResult = sdk.reconcileReportingCoreV1(coreInput); + if (nativeResult.health !== 'complete' || nativeResult.obligations.length !== 1 || + nativeResult.obligations[0].satisfied !== true) { + throw new Error('Core native reconciliation did not satisfy the deterministic obligation'); + } + if (JSON.stringify(coreInput) !== inputBefore) { + throw new Error('Core reconciler mutated caller-owned input'); + } + const coverageIncomplete = sdk.reconcileReportingCoreV1({ + ...structuredClone(coreInput), + scope: { ...coreInput.scope, coverageComplete: false }, + }); + if (coverageIncomplete.health !== 'action_required' || + coverageIncomplete.obligations[0]?.satisfied !== true || + coverageIncomplete.obligations[0]?.health !== 'complete') { + throw new Error('Core coverage-incomplete top-level control changed semantics'); + } + let incompleteRecordsError; + try { + sdk.reconcileReportingCoreV1({ + ...structuredClone(coreInput), + scope: { ...coreInput.scope, recordsComplete: false }, + }); + } catch (error) { + incompleteRecordsError = String(error.message || error); + } + if (incompleteRecordsError !== + 'Core reporting reconciliation requires every reporting-status cursor page') { + throw new Error('Core incomplete-cursor control did not fail closed'); + } + const partialCoverageInput = structuredClone(coreInput); + partialCoverageInput.obligations[0].coverage = { status: 'partial' }; + const partialCoverage = sdk.reconcileReportingCoreV1(partialCoverageInput); + if (partialCoverage.obligations[0]?.satisfied !== false || + !partialCoverage.obligations[0]?.issues?.some( + issue => issue.code === 'REPORTING_COVERAGE_INCOMPLETE')) { + throw new Error('Core partial-coverage control did not report the declared issue'); + } + const obligation = nativeResult.obligations[0]; + const neutralSatisfied = nativeResult.health === 'complete' && + nativeResult.obligations.every(item => item.satisfied === true); + return { + core_client_subpath: coreSubpath, + core_reconciler_export: typeof sdk.reconcileReportingCoreV1, + kind: 'reporting_interop_ts_core_buyer', + native_result: nativeResult, + native_controls: { + caller_input_mutated: false, + coverage_incomplete: coverageIncomplete, + incomplete_records_error: incompleteRecordsError, + partial_coverage: partialCoverage, + }, + neutral_result: { + health: nativeResult.health, + obligations: nativeResult.obligations.map(item => ({ + health: item.health, + reporting_revision_ids: item.reportingRevisionIds, + requirements_satisfied: item.satisfied, + })), + requirements_satisfied: neutralSatisfied, + selected_obligation_satisfied: obligation.satisfied, + }, + package: { + integrity: context.expectedIntegrity, + name: context.packageName, + version: context.installed.version, + }, + raw_facts: rawFacts, + runtime: { node: process.version }, + schema_version: 1, + semantic_lane_complete: true, + status: 'passed', + }; +} + +(async () => { + try { + process.stdout.write(`${JSON.stringify(stable(await run()), null, 2)}\n`); + } catch (error) { + process.stdout.write(`${JSON.stringify(stable({ + error: { message: String(error.message || error), name: error.name || 'Error' }, + kind: 'reporting_interop_ts_core_buyer', + schema_version: 1, + status: 'failed', + }), null, 2)}\n`); + process.exitCode = 2; + } +})(); diff --git a/scripts/ci/reporting_interop/ts_core_server.cjs b/scripts/ci/reporting_interop/ts_core_server.cjs new file mode 100755 index 000000000..2fe895938 --- /dev/null +++ b/scripts/ci/reporting_interop/ts_core_server.cjs @@ -0,0 +1,643 @@ +#!/usr/bin/env node +'use strict'; + +/* + * Installed-package TypeScript Reliable Reporting Core seller. + * + * This is intentionally a Core-only composition. It uses the public ledger, + * source, schema, and MCP server exports from the package selected by + * --package-lock. It does not import package internals, use the mock server, + * or opt into ReliableReportingService / Managed Delivery. + */ + +const fs = require('node:fs'); +const path = require('node:path'); +const { createRequire } = require('node:module'); +const crypto = require('node:crypto'); +const { assertFixtureCapabilityVersion, wireAdcpVersion } = require('./ts_adcp_version.cjs'); +const { corePrivateInputs } = require('./ts_private_inputs.cjs'); + +const FIXED_NOW = '2026-09-03T00:00:00.000Z'; +const PERIOD_START = '2026-08-01T00:00:00.000Z'; +const PERIOD_END = '2026-08-02T00:00:00.000Z'; +const DAY_MILLISECONDS = 86_400_000; +const ACCOUNTS = ['interop-account-a', 'interop-account-b']; +const PRINCIPAL_BINDINGS = [ + { account_id: ACCOUNTS[0], consumer_id: 'https://buyer-a.example.test/adcp' }, + { account_id: ACCOUNTS[1], consumer_id: 'opaque-consumer-b' }, +]; + +function parseArgs(argv) { + const values = { host: '127.0.0.1', package: '@adcp/sdk', probe: false }; + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (token === '--probe') { + values.probe = true; + continue; + } + if (!token.startsWith('--')) throw new Error(`unexpected positional argument: ${token}`); + const key = token.slice(2); + const value = argv[index + 1]; + if (!value || value.startsWith('--')) throw new Error(`missing value for --${key}`); + if (Object.hasOwn(values, key) && !['host', 'package'].includes(key)) { + throw new Error(`duplicate argument: --${key}`); + } + values[key] = value; + index += 1; + } + return values; +} + +function requireArg(args, name) { + if (!args[name]) throw new Error(`missing required argument: --${name}`); + return args[name]; +} + +function positivePort(value) { + const port = Number(value); + if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) { + throw new Error('--port must be an integer from 1 through 65535'); + } + return port; +} + +function stable(value) { + if (Array.isArray(value)) return value.map(stable); + if (value && typeof value === 'object') { + return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])])); + } + return value; +} + +function emit(stream, value) { + stream.write(`${JSON.stringify(stable(value))}\n`); +} + +function packageContext(args) { + const packageName = args.package; + const expectedVersion = requireArg(args, 'expected-version'); + const expectedIntegrity = requireArg(args, 'expected-integrity'); + const lockPath = path.resolve(requireArg(args, 'package-lock')); + const packageRequire = createRequire(path.join(path.dirname(lockPath), 'package.json')); + const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + const lockEntry = lock.packages?.[`node_modules/${packageName}`]; + if (!lockEntry) throw new Error(`package lock does not contain ${packageName}`); + + const packageFile = packageRequire.resolve(`${packageName}/package.json`); + const packageRoot = fs.realpathSync(path.dirname(packageFile)); + const installed = packageRequire(packageFile); + if (installed.name !== packageName || installed.version !== expectedVersion || + lockEntry.version !== expectedVersion || lockEntry.integrity !== expectedIntegrity) { + throw new Error('refusing to execute an unpinned SDK installation'); + } + for (const subpath of [ + packageName, + `${packageName}/reporting/ledger`, + `${packageName}/reporting/source`, + `${packageName}/schemas`, + `${packageName}/server`, + ]) { + const resolved = fs.realpathSync(packageRequire.resolve(subpath)); + if (!resolved.startsWith(`${packageRoot}${path.sep}`)) { + throw new Error(`public package entry resolved outside the installed package root: ${subpath}`); + } + } + return { + expectedIntegrity, + expectedVersion, + installed, + lockPath, + packageName, + packageRequire, + packageRoot, + }; +} + +function loadPublicSurface(context, adcpVersion) { + const sdk = context.packageRequire(context.packageName); + const ledger = context.packageRequire(`${context.packageName}/reporting/ledger`); + const source = context.packageRequire(`${context.packageName}/reporting/source`); + const schemas = context.packageRequire(`${context.packageName}/schemas`); + const server = context.packageRequire(`${context.packageName}/server`); + const pg = context.packageRequire('pg'); + const required = [ + [ledger, 'PostgresReportingLedgerStore'], + [ledger, 'createReportingProducer'], + [ledger, 'createReportingDeliveryHandler'], + [ledger, 'createReportingStatusHandler'], + [ledger, 'createSyncReportingStatusHandler'], + [source, 'createInlineReportingSourceExecutor'], + [schemas, 'getCanonicalToolValidator'], + [server, 'createTaskCapableServer'], + [server, 'legacyCapabilitiesResponse'], + [server, 'serve'], + [server, 'toStructuredContent'], + [server, 'verifyApiKey'], + [pg, 'Pool'], + ]; + for (const [container, name] of required) { + if (typeof container[name] !== 'function') { + throw new Error(`required public installed-package export is unavailable: ${name}`); + } + } + for (const name of ['REPORTING_LEDGER_MIGRATION', 'REPORTING_LEDGER_FINALITY_WRITER_FENCE_MIGRATION']) { + if (typeof ledger[name] !== 'string' || ledger[name].length === 0) { + throw new Error(`required public installed-package export is unavailable: ${name}`); + } + } + for (const tool of [ + 'get_adcp_capabilities', + 'get_media_buy_delivery', + 'get_reporting_status', + 'sync_reporting_status', + ]) { + if (!schemas.TOOL_INPUT_SHAPES?.[tool]) { + throw new Error(`required public canonical input shape is unavailable: ${tool}`); + } + } + if (sdk.ADCP_VERSION !== adcpVersion) { + throw new Error(`installed SDK protocol ${String(sdk.ADCP_VERSION)} does not match --adcp-version`); + } + return { ledger, pg, schemas, sdk, server, source }; +} + +function reportingContract() { + return { + report_definition_id: 'interop-core-v1', + reportDefinitionUri: 'https://contracts.example/interop-core-v1.json', + reportDefinitionSha256: 'a'.repeat(64), + reportingProfile: 'interop-core-v1', + schemaVersion: '1', + schemaUri: 'https://contracts.example/interop-core-rows-v1.json', + schemaSha256: 'b'.repeat(64), + schemaDialect: 'https://json-schema.org/draft/2020-12/schema', + schemaRefPolicy: 'local_fragment_only', + mappingId: 'interop-core-v1', + mappingVersion: '1', + mappingSha256: 'c'.repeat(64), + }; +} + +function sourceOffering(source) { + const offering = structuredClone(source.redactedReportingSourceOfferingV1); + offering.offeringId = 'interop-core-daily'; + offering.publicationNamespace = 'reporting-source:interop-core'; + offering.applicability.productIds = ['interop-product']; + offering.contract = reportingContract(); + offering.dimensions = [ + { name: 'media_buy_id', support: 'exact' }, + { name: 'vendor', support: 'exact' }, + ]; + return offering; +} + +function constituent(accountId) { + const suffix = accountId.at(-1); + const mediaBuyId = `media-buy-${suffix}`; + return { + constituentId: `interop-constituent-${suffix}`, + constituentKind: 'media_buy', + productId: 'interop-product', + mediaBuyId, + productBinding: { + owner: 'caller', + bindingId: `interop-product-binding-${suffix}`, + bindingVersion: 1, + bindingSha256: 'c'.repeat(64), + productId: 'interop-product', + bindingKind: 'media_buy_product', + mediaBuyId, + }, + }; +} + +function configurationInput(accountId, offering, source) { + const suffix = accountId.at(-1); + const template = source.redactedReportingSourceRequestV1(); + return { + account: { account_id: accountId }, + sourceScope: { connection: `interop-${suffix}`, region: 'test' }, + delivery_config_id: 'shared-daily-key', + delivery_config_version: 1, + offeringId: offering.offeringId, + report_definition_id: 'interop-core-v1', + feedPurpose: 'analytics', + requiredFinality: 'snapshot', + requestedMetrics: ['impressions'], + requestedDimensions: ['media_buy_id'], + constituents: [constituent(accountId)], + mediaBuyIds: [`media-buy-${suffix}`], + sourceTimezone: 'UTC', + schedule: { + anchor: PERIOD_START, + periodMilliseconds: DAY_MILLISECONDS, + deliverySlaMilliseconds: 3_600_000, + recoveryWindowMilliseconds: DAY_MILLISECONDS, + periodDuration: 'P1D', + alignment: 'utc', + deliverySlaDuration: 'PT1H', + }, + sourceSettings: structuredClone(template.sourceSettings), + contract: structuredClone(offering.contract), + }; +} + +function capabilities(adcpVersion) { + const wireVersion = wireAdcpVersion(adcpVersion); + return { + adcp_version: wireVersion, + adcp: { + major_versions: [3], + supported_versions: [wireVersion], + idempotency: { supported: false }, + }, + supported_protocols: ['media_buy'], + experimental_features: ['media_buy.reporting_delivery'], + account: { + require_operator_auth: true, + supported_billing: ['operator'], + supported_account_currency_modes: ['fixed'], + timezone: { mode: 'seller_fixed', fixed_timezone: 'UTC' }, + required_for_products: true, + sandbox: false, + }, + media_buy: { + reporting_delivery: { + supported: true, + reliable_reporting_version: '1.0', + managed_delivery: false, + reconciled_billing: false, + configuration_task: 'sync_accounts', + status_task: 'get_reporting_status', + consumer_status_task: 'sync_reporting_status', + revision_content_task: 'get_media_buy_delivery', + offerings: [{ + offering_id: 'interop-core-daily', + feed_purpose: 'analytics', + report_definition_id: 'interop-core-v1', + report_definition_uri: 'https://contracts.example/interop-core-v1.json', + report_definition_sha256: 'a'.repeat(64), + reporting_profile: { + id: 'interop-core-v1', + version: '1', + schema_uri: 'https://contracts.example/interop-core-rows-v1.json', + schema_sha256: 'b'.repeat(64), + schema_dialect: 'https://json-schema.org/draft/2020-12/schema', + schema_ref_policy: 'local_fragment_only', + grain: 'media_buy', + primary_keys: ['media_buy_id'], + }, + schedule: { period_duration: 'P1D', alignment: 'utc', delivery_sla: 'PT1H' }, + supported_finality: ['snapshot'], + reconciliation_mode: 'delivery_only', + }], + automated_recovery_window_seconds: 86_400, + status_retention_days: 30, + }, + }, + }; +} + +function validateAdvertisedCapabilities(publicApi, adcpVersion, + advertised = capabilities(adcpVersion)) { + assertFixtureCapabilityVersion(advertised, adcpVersion); + const validator = publicApi.schemas.getCanonicalToolValidator( + 'get_adcp_capabilities', 'sync', { adcpVersion }, + ); + if (typeof validator !== 'function' || !validator({ ...advertised, status: 'completed' })) { + throw new Error(`Core capability document failed the installed canonical validator: ${JSON.stringify(validator?.errors || [])}`); + } + return advertised; +} + +function authenticationBindings(privateInputs) { + // Entropy is supplied by the trusted launcher (secrets.token_urlsafe(32)). + // ts_private_inputs validates minimum length/distinctness before database + // allocation. A readiness digest is an identity binding, not added entropy. + const tokens = [privateInputs.tokenA, privateInputs.tokenB]; + return new Map(tokens.map((token, index) => [token, PRINCIPAL_BINDINGS[index]])); +} + +function buildReadyRecord(context, { + adcpVersion, authBindings, port, sellerRole, startupProof, url, +}) { + return { + accounts: ACCOUNTS, + auth_binding_sha256: Object.fromEntries( + [...authBindings].map(([token, binding]) => [ + binding.account_id, + crypto.createHash('sha256').update(token).digest('hex'), + ]), + ), + kind: 'reporting_interop_ts_core_server_ready', + node: { executable: fs.realpathSync(process.execPath), version: process.version }, + pid: process.pid, + port, + process_start_token: processStartToken(), + seller_package: { + integrity: context.expectedIntegrity, + language: 'typescript', + name: context.packageName, + package_role: sellerRole, + protocol: adcpVersion, + version: context.installed.version, + wire_adcp_version: wireAdcpVersion(adcpVersion), + }, + startup_proof_sha256: crypto.createHash('sha256').update(startupProof).digest('hex'), + status: 'ready', + url, + }; +} + +function processStartToken() { + if (process.platform !== 'linux') { + throw new Error('Core seller owned-start proof currently requires Linux /proc'); + } + const stat = fs.readFileSync(`/proc/${process.pid}/stat`, 'utf8'); + const commandEnd = stat.lastIndexOf(')'); + const fields = commandEnd < 0 ? [] : stat.slice(commandEnd + 2).trim().split(/\s+/); + const startToken = fields[19]; // proc(5) field 22; fields starts at field 3. + if (!/^\d+$/.test(startToken || '')) { + throw new Error('Core seller could not establish its Linux process start identity'); + } + return startToken; +} + +function mcpSuccess(server, data, summary) { + return { + content: [{ type: 'text', text: summary }], + structuredContent: server.toStructuredContent(data), + }; +} + +function fixedInstallStore(store, diagnostic) { + return new Proxy(store, { + get(target, property) { + if (property === 'putConfiguration') { + return configuration => target.putConfiguration({ ...configuration, installedAt: PERIOD_START }); + } + const value = Reflect.get(target, property, target); + if (typeof value !== 'function') return value; + return async (...args) => { + try { + return await value.apply(target, args); + } catch (error) { + diagnostic.error = error; + diagnostic.operation = String(property); + throw error; + } + }; + }, + }); +} + +async function prepareDatabase(publicApi, databaseUrl) { + const pool = new publicApi.pg.Pool({ + connectionString: databaseUrl, + max: 4, + application_name: 'adcp-reporting-interop-ts-core', + }); + try { + await pool.query(publicApi.ledger.REPORTING_LEDGER_MIGRATION); + await pool.query(publicApi.ledger.REPORTING_LEDGER_FINALITY_WRITER_FENCE_MIGRATION); + const occupied = await pool.query(` + SELECT + (SELECT count(*)::int FROM adcp_reporting_configurations) AS configurations, + (SELECT count(*)::int FROM adcp_reporting_obligations) AS obligations, + (SELECT count(*)::int FROM adcp_reporting_revisions) AS revisions + `); + if (Object.values(occupied.rows[0]).some(value => Number(value) !== 0)) { + throw new Error('reporting interoperability server requires a fresh isolated PostgreSQL database'); + } + + const store = new publicApi.ledger.PostgresReportingLedgerStore(pool, { + acknowledgeIsolatedDatabase: true, + }); + const offering = sourceOffering(publicApi.source); + const sourceExecutor = publicApi.source.createInlineReportingSourceExecutor(input => ({ + reporting_period: { start: input.start_date, end: input.end_date }, + currency: 'USD', + reporting_rows: [{ + media_buy_id: input.media_buy_ids[0], + impressions: 5, + vendor: { fraction: '1.25', enabled: true, ordinal: 1 }, + }], + data_through: input.end_date, + observed_at: input.end_date, + }), offering); + let sourceFailure; + const executeSource = sourceExecutor.execute.bind(sourceExecutor); + sourceExecutor.execute = async (...input) => { + const result = await executeSource(...input); + if (!result.ok) sourceFailure = result.error; + return result; + }; + const storeDiagnostic = {}; + const producer = publicApi.ledger.createReportingProducer({ + store: fixedInstallStore(store, storeDiagnostic), + source: sourceExecutor, + offerings: [offering], + contact: { name: 'Reporting interoperability operations' }, + }); + + for (const accountId of ACCOUNTS) { + await producer.installConfiguration(configurationInput(accountId, offering, publicApi.source)); + const planned = await producer.planObligations(PERIOD_END, { account_id: accountId, maxObligations: 2 }); + if (planned.length !== 1 || planned[0].period.start !== PERIOD_START || planned[0].period.end !== PERIOD_END) { + throw new Error(`failed to seed the deterministic reporting obligation for ${accountId}`); + } + const worker = await producer.runWorker({ + account_id: accountId, + maxIterations: 2, + now: () => new Date(FIXED_NOW), + }); + if (worker.revisionsCommitted !== 1 || worker.failed !== 0 || worker.reconcilesDeferred !== 0) { + const storeMessage = storeDiagnostic.error + ? `${storeDiagnostic.operation}: ${String(storeDiagnostic.error?.message || storeDiagnostic.error)}` + : undefined; + const sourceMessage = sourceFailure?.message || sourceFailure?.safeMessage || storeMessage; + const detail = sourceFailure?.code + ? `: source ${sourceFailure.code}${sourceMessage ? ` (${sourceMessage})` : ''}` + : ''; + throw new Error(`failed to seed the deterministic reporting revision for ${accountId}${detail}`); + } + } + return { pool, store }; + } catch (error) { + await pool.end().catch(() => {}); + throw error; + } +} + +function createAgent(publicApi, store, advertised) { + const resolveConsumerId = context => { + const consumerId = context?.authInfo?.extra?.consumer_id; + if (typeof consumerId !== 'string' || consumerId.length === 0) { + throw new TypeError('reporting status requires an authenticated consumer principal'); + } + return consumerId; + }; + const status = publicApi.ledger.createReportingStatusHandler(store, { + resolveConsumerId, + }); + const delivery = publicApi.ledger.createReportingDeliveryHandler(store); + const syncStatus = publicApi.ledger.createSyncReportingStatusHandler(store, { + resolveConsumerId, + }); + return () => { + const mcp = publicApi.server.createTaskCapableServer( + 'Reporting interoperability TypeScript Core seller', + '1.0.0', + ); + mcp.registerTool('get_adcp_capabilities', { + description: 'Return the installed seller capability document.', + inputSchema: publicApi.schemas.TOOL_INPUT_SHAPES.get_adcp_capabilities, + }, async () => publicApi.server.legacyCapabilitiesResponse(advertised)); + mcp.registerTool('get_reporting_status', { + description: 'Read the durable Reliable Reporting Core ledger.', + inputSchema: publicApi.schemas.TOOL_INPUT_SHAPES.get_reporting_status, + }, async (request, extra) => { + const accountId = request?.account?.account_id; + const authorizedAccount = extra?.authInfo?.extra?.account_id; + if (typeof accountId !== 'string' || accountId !== authorizedAccount) { + throw new Error('caller is not authorized for this reporting account'); + } + const result = await status(request, { + account: { account_id: accountId }, + authInfo: extra.authInfo, + }); + return mcpSuccess(publicApi.server, result, 'Reporting status retrieved'); + }); + mcp.registerTool('get_media_buy_delivery', { + description: 'Read immutable rows for an exact reporting revision.', + inputSchema: publicApi.schemas.TOOL_INPUT_SHAPES.get_media_buy_delivery, + }, async (request, extra) => { + const accountId = request?.account?.account_id; + const authorizedAccount = extra?.authInfo?.extra?.account_id; + if (typeof accountId !== 'string' || accountId !== authorizedAccount) { + throw new Error('caller is not authorized for this reporting account'); + } + const result = await delivery(request, { + account: { account_id: accountId }, + authInfo: extra.authInfo, + }); + return mcpSuccess(publicApi.server, result, 'Reporting revision content retrieved'); + }); + mcp.registerTool('sync_reporting_status', { + description: 'Persist authenticated consumer reporting status.', + inputSchema: publicApi.schemas.TOOL_INPUT_SHAPES.sync_reporting_status, + }, async (request, extra) => { + const accountId = request?.account?.account_id; + const authorizedAccount = extra?.authInfo?.extra?.account_id; + if (typeof accountId !== 'string' || accountId !== authorizedAccount) { + throw new Error('caller is not authorized for this reporting account'); + } + const result = await syncStatus(request, { + account: { account_id: accountId }, + authInfo: extra.authInfo, + }); + return mcpSuccess(publicApi.server, result, 'Reporting consumer status synchronized'); + }); + return mcp; + }; +} + +async function run(argv = process.argv.slice(2), dependencies = {}) { + const args = parseArgs(argv); + const context = (dependencies.packageContext || packageContext)(args); + const adcpVersion = requireArg(args, 'adcp-version'); + const publicApi = (dependencies.loadPublicSurface || loadPublicSurface)(context, adcpVersion); + const advertised = validateAdvertisedCapabilities(publicApi, adcpVersion); + + if (args.probe) { + (dependencies.emit || emit)(dependencies.stdout || process.stdout, { + adcp_version: adcpVersion, + kind: 'reporting_interop_ts_core_server_probe', + package: { + integrity: context.expectedIntegrity, + name: context.packageName, + version: context.installed.version, + }, + public_composition: { + postgres_ledger: true, + producer: true, + status_handler: true, + consumer_status_handler: true, + mcp_http_server: true, + bearer_auth: true, + reliable_reporting_service: false, + }, + status: 'passed', + }); + return; + } + + const port = positivePort(requireArg(args, 'port')); + const privateInputs = corePrivateInputs(dependencies.environment || process.env); + const authBindings = authenticationBindings(privateInputs); + const startupProof = privateInputs.startupProof; + const readyFile = path.resolve(requireArg(args, 'ready-file')); + const sellerRole = requireArg(args, 'seller-role'); + if (!new Set(['candidate', 'historical_candidate', 'previous', 'lead']).has(sellerRole)) { + throw new Error('Core seller role is not a declared immutable package role'); + } + const databaseUrl = args['database-url'] || process.env.DATABASE_URL; + if (!databaseUrl) throw new Error('--database-url or DATABASE_URL is required'); + const { pool, store } = await prepareDatabase(publicApi, databaseUrl); + const authenticate = publicApi.server.verifyApiKey({ + verify(token) { + const binding = authBindings.get(token); + if (!binding) return null; + return { + principal: `principal-${binding.account_id.at(-1)}`, + extra: structuredClone(binding), + }; + }, + }); + const httpServer = publicApi.server.serve(createAgent(publicApi, store, advertised), { + allowedHosts: [args.host, 'localhost', '127.0.0.1'], + authenticate, + path: '/mcp', + port, + readinessCheck: async () => { await pool.query('SELECT 1'); }, + onListening(url) { + const ready = buildReadyRecord(context, { + adcpVersion, authBindings, port, sellerRole, startupProof, url, + }); + fs.writeFileSync(readyFile, `${JSON.stringify(stable(ready))}\n`, { + flag: 'wx', mode: 0o600, + }); + emit(process.stdout, ready); + }, + }); + + let closing = false; + async function close() { + if (closing) return; + closing = true; + await new Promise(resolve => httpServer.close(resolve)); + await pool.end(); + } + process.once('SIGINT', () => { close().then(() => process.exit(0), () => process.exit(1)); }); + process.once('SIGTERM', () => { close().then(() => process.exit(0), () => process.exit(1)); }); +} + +if (require.main === module) { + run().catch(error => { + emit(process.stderr, { + error: { message: String(error?.message || error), name: error?.name || 'Error' }, + kind: 'reporting_interop_ts_core_server_error', + status: 'failed', + }); + process.exitCode = 1; + }); +} + +module.exports = { + authenticationBindings, + buildReadyRecord, + capabilities, + corePrivateInputs, + run, + validateAdvertisedCapabilities, +}; diff --git a/scripts/ci/reporting_interop/ts_managed_reporting_fixture.cjs b/scripts/ci/reporting_interop/ts_managed_reporting_fixture.cjs new file mode 100644 index 000000000..300322801 --- /dev/null +++ b/scripts/ci/reporting_interop/ts_managed_reporting_fixture.cjs @@ -0,0 +1,471 @@ +'use strict'; + +/* + * PostgreSQL-backed Managed Delivery / Reconciled Billing controller fixture. + * + * This module deliberately uses only public rc.45 exports. Controller setup + * creates immutable Core facts through the public stores, while delivery, + * resource reads, revocation, status projection, receipts, and adjustment + * readback traverse the public runtime/handler contracts that the storyboards + * exercise. It is not a virtual-clock fixture and does not claim DST coverage. + */ + +const { createHash } = require('node:crypto'); + +const ACCOUNT_ID = 'reporting_core_lab'; +const CONSUMER_ID = 'https://buyer.example.test/adcp'; +const CANONICAL_DIGEST = { + algorithm: 'sha256', + value: 'bcd079902f3c8edb4315dbbdaf9b4e37f6fd5af33c80d1fe6ac8c655581342d4', + canonicalization_id: 'billing-rows-v1', + canonicalization_uri: 'https://test-agent.adcontextprotocol.org/reporting/canonicalization/billing-rows-v1.json', + canonicalization_sha256: 'f98d55f8b7ad49c058b17f04630576c6b27b3564090b838e49834dd7714cdefa', +}; + +function iso(value) { + return new Date(value).toISOString(); +} + +function sha256(value) { + return createHash('sha256').update(value).digest('hex'); +} + +function deliveryOffering(mode) { + const billing = mode === 'billing'; + return { + offering_id: billing ? 'billing-files-v1' : 'managed-files-v1', + feed_purpose: billing ? 'billing' : 'analytics', + report_definition_id: billing ? 'billing-v1' : 'managed-analytics-v1', + report_definition_uri: 'https://schemas.fixture.example/report-definition.json', + report_definition_sha256: 'd'.repeat(64), + reporting_profile: { + id: billing ? 'billing-v1' : 'managed-analytics-v1', + version: '1.0', + schema_uri: 'https://schemas.fixture.example/reporting-profile.json', + schema_sha256: 'e'.repeat(64), + schema_dialect: 'https://json-schema.org/draft/2020-12/schema', + schema_ref_policy: 'local_fragment_only', + grain: 'media_buy/day', + primary_keys: ['media_buy_id'], + ...(billing ? { + canonicalization_id: CANONICAL_DIGEST.canonicalization_id, + canonicalization_contract_version: '1.0', + canonicalization_media_type: 'application/vnd.adcp.reporting-canonicalization+json', + canonicalization_uri: CANONICAL_DIGEST.canonicalization_uri, + canonicalization_sha256: CANONICAL_DIGEST.canonicalization_sha256, + } : {}), + }, + schedule: { period_duration: 'PT30M', alignment: 'utc', delivery_sla: 'PT0S' }, + supported_finality: ['official'], + reconciliation_mode: billing ? 'consumer_receipt' : 'delivery_only', + method: { + pattern: 'file_transfer', + transport: 'fixture_object_store', + orchestration: 'producer_managed', + destination_modes: ['existing'], + provider: { domain: 'fixture.example' }, + format: 'jsonl', + }, + }; +} + +function makeProvider() { + const resources = new Map(); + const grants = new Set(); + const events = []; + return { + resources, + grants, + events, + adapter: { + verificationProfiles: ['canonical_digest'], + revocationFencesDeliveryGenerations: true, + async deliver(input) { + const key = `${input.binding.destination_ref}:${input.binding.authorization_generation}`; + const resourceRef = `resource:${input.materialization.reporting_materialization_id}`; + const bytes = Buffer.from(input.revision.rows.map(row => JSON.stringify(row)).join('\n') + '\n', 'utf8'); + grants.add(key); + resources.set(resourceRef, { bytes, key }); + events.push({ kind: 'deliver', resource_ref: resourceRef, destination_ref: input.binding.destination_ref }); + return { + status: 'available', + resource: { + resource_ref: resourceRef, + kind: 'manifest', + location: `fixture.reports.${input.revision.reporting_revision_id}.manifest`, + manifest_version: '1.0', + manifest_sha256: sha256(bytes), + immutability: 'immutable_location', + expires_at: iso(Date.now() + 31 * 86_400_000), + }, + verification: { + verified_at: iso(Date.now()), + verification_path: 'representative_consumer', + verification_profile: 'canonical_digest', + row_count: input.revision.wireRevision.row_count, + control_totals: structuredClone(input.revision.wireRevision.control_totals), + physical_checksums: [{ object_ref: 'rows.jsonl', algorithm: 'sha256', value: sha256(bytes) }], + canonical_content_digest: structuredClone(input.revision.wireRevision.canonical_content_digest), + }, + }; + }, + async read(input) { + const found = resources.get(input.resource.resource_ref); + if (!found || !grants.has(found.key)) throw new Error('provider grant is unavailable'); + if (found.bytes.byteLength > input.maxBytes) throw new Error('provider object exceeds the requested limit'); + events.push({ kind: 'read', resource_ref: input.resource.resource_ref }); + return Uint8Array.from(found.bytes); + }, + async revoke({ authorization }) { + const key = `${authorization.destination_ref}:${authorization.generation}`; + grants.delete(key); + events.push({ kind: 'revoke', destination_ref: authorization.destination_ref }); + }, + }, + }; +} + +async function createManagedReportingFixture(api, pool, { mode }) { + if (!['managed', 'billing'].includes(mode)) throw new Error(`unsupported fixture mode: ${String(mode)}`); + await pool.query(api.ledger.REPORTING_LEDGER_MIGRATION); + await pool.query(api.ledger.REPORTING_LEDGER_FINALITY_WRITER_FENCE_MIGRATION); + await pool.query(api.ledger.REPORTING_MANAGED_DELIVERY_MIGRATION); + + const core = new api.ledger.PostgresReportingLedgerStore(pool, { + acknowledgeIsolatedDatabase: true, + managedDelivery: true, + }); + const managed = new api.ledger.PostgresReportingManagedDeliveryStore(pool, { + evidenceRetentionDays: 90, + statusRetentionDays: 90, + }); + const provider = makeProvider(); + const offering = deliveryOffering(mode); + const runtime = await api.ledger.createReportingManagedDeliveryRuntime({ + coreStore: core, + store: managed, + adapter: provider.adapter, + offerings: [offering], + automatedRecoveryWindowSeconds: 60, + statusRetentionDays: 90, + resourceRetentionDays: 30, + authorizationRevocationSeconds: 60, + resolveConsumerId: context => context.consumer_id, + }); + const context = { account: { account_id: ACCOUNT_ID }, consumer_id: CONSUMER_ID }; + const state = { + prepared: false, + readinessNotificationSuppressed: false, + adjustments: [], + }; + + async function status(view = 'periods', extra = {}) { + return runtime.getReportingStatus({ account: { account_id: ACCOUNT_ID }, view, ...extra }, context); + } + + async function prepare() { + if (state.prepared) return state; + const nowMs = Date.now(); + const now = iso(nowMs); + // Keep exactly one elapsed period. Unfiltered periods reads fail closed if + // the configuration implies another elapsed obligation that the fixture + // did not persist, so the next 30-minute boundary remains in the future. + const period = { start: iso(nowMs - 1_860_000), end: iso(nowMs - 60_000) }; + const billing = mode === 'billing'; + const prefix = billing ? 'billing' : 'managed'; + const destinationRef = `destination-${prefix}-generation-1`; + const configuration = { + configurationId: `configuration-${prefix}-1`, + account: { account_id: ACCOUNT_ID }, + sourceScope: { warehouse: `fixture-${prefix}` }, + delivery_config_id: `${prefix}-files`, + delivery_config_version: 1, + offeringId: offering.offering_id, + report_definition_id: offering.report_definition_id, + feedPurpose: offering.feed_purpose, + requiredFinality: 'official', + ...(billing ? { + canonicalization: { + id: CANONICAL_DIGEST.canonicalization_id, + uri: CANONICAL_DIGEST.canonicalization_uri, + sha256: CANONICAL_DIGEST.canonicalization_sha256, + primaryKeys: ['media_buy_id'], + }, + } : {}), + requestedMetrics: billing ? ['impressions', 'spend'] : ['impressions'], + requestedDimensions: ['media_buy_id'], + constituents: [], + mediaBuyIds: ['buy-1', 'buy-2'], + sourceTimezone: 'UTC', + schedule: { + anchor: period.start, + periodMilliseconds: 1_800_000, + deliverySlaMilliseconds: 0, + recoveryWindowMilliseconds: 60_000, + }, + sourceSettings: {}, + contract: { reportingProfile: offering.reporting_profile.id }, + installedAt: period.start, + semanticFingerprint: `configuration-${prefix}-fingerprint`, + }; + await core.putConfiguration(configuration); + await managed.authorizeDestination({ + account_id: ACCOUNT_ID, + destination_ref: destinationRef, + generation: 1, + authorized_at: now, + }); + const binding = api.ledger.reportingManagedDeliveryBindingV1({ + configurationId: configuration.configurationId, + account_id: ACCOUNT_ID, + delivery_config_id: configuration.delivery_config_id, + delivery_config_version: 1, + destination_ref: destinationRef, + authorization_generation: 1, + feed_purpose: offering.feed_purpose, + method: 'file_transfer', + transport: 'fixture_object_store', + verification_profile: 'canonical_digest', + reconciliation_mode: billing ? 'consumer_receipt' : 'delivery_only', + resource_retention_days: 30, + created_at: now, + }); + await managed.installBinding(binding); + const obligation = { + reporting_obligation_id: `obligation-${prefix}-1`, + configurationId: configuration.configurationId, + account: configuration.account, + sourceScope: configuration.sourceScope, + delivery_config_id: configuration.delivery_config_id, + delivery_config_version: 1, + offeringId: configuration.offeringId, + report_definition_id: configuration.report_definition_id, + feedPurpose: configuration.feedPurpose, + requiredFinality: 'official', + periodOrdinal: 0, + period: { ...period, sourceTimezone: 'UTC' }, + schedule: configuration.schedule, + scopeResolvedAt: period.end, + coverage: { + status: 'full', evaluatedAt: period.end, + mediaBuyIds: ['buy-1', 'buy-2'], fullyCoveredMediaBuyIds: ['buy-1', 'buy-2'], + partiallyCoveredMediaBuyIds: [], unsupportedMediaBuyIds: [], unknownMediaBuyIds: [], + }, + requestedMetrics: configuration.requestedMetrics, + requestedDimensions: configuration.requestedDimensions, + constituents: [], + mediaBuyIds: configuration.mediaBuyIds, + sourceSettings: {}, + contract: configuration.contract, + expectedAt: period.end, + recoveryDeadlineAt: iso(Date.parse(period.end) + 60_000), + publicationOffsets: [], nextAttemptAt: period.end, attemptCount: 0, state: 'pending', + semanticFingerprint: `obligation-${prefix}-fingerprint`, createdAt: now, + }; + await core.putObligation(obligation); + const lease = await core.claimObligation({ + owner: `${prefix}-core-worker`, now, leaseMilliseconds: 600_000, account_id: ACCOUNT_ID, + }); + if (!lease) throw new Error('public Core store did not lease the prepared obligation'); + const rows = [ + { media_buy_id: 'buy-1', impressions: 2, ...(billing ? { spend: '3.00' } : {}) }, + { media_buy_id: 'buy-2', impressions: 3, ...(billing ? { spend: '5.00' } : {}) }, + ]; + const controlTotals = [ + { name: 'impressions', value: '5', value_type: 'integer', unit: 'impressions' }, + ...(billing ? [{ name: 'spend', value: '8.00', value_type: 'decimal', unit: 'USD' }] : []), + ]; + const revisionId = `revision-${prefix}-official-1`; + const revisionBytes = Buffer.from(api.jcs.canonicalize({ + reporting_revision_id: revisionId, + row_count: rows.length, + control_totals: controlTotals, + reporting_rows: rows, + }), 'utf8'); + const revision = { + reporting_revision_id: revisionId, + reporting_obligation_id: obligation.reporting_obligation_id, + revisionNumber: 1, + finality: 'official', kind: 'official', + manifest: { level: 'basic', objectRef: `${prefix}-manifest`, sha256: 'a'.repeat(64), byteCount: 1 }, + sourcePublicationId: `publication-${prefix}-1`, + binding: { + algorithm: 'rfc8785_jcs_v1', sha256: sha256(revisionBytes), + byteCount: revisionBytes.byteLength, rowCount: rows.length, + }, + rows, observedAt: now, dataThrough: period.end, sourceReadCutoffAt: now, createdAt: now, + wireRevision: { + reporting_revision_id: revisionId, + revision_content_sha256: sha256(revisionBytes), + report_definition_id: offering.report_definition_id, + report_definition_uri: offering.report_definition_uri, + report_definition_sha256: offering.report_definition_sha256, + reporting_profile: offering.reporting_profile.id, + schema_version: offering.reporting_profile.version, + schema_uri: offering.reporting_profile.schema_uri, + schema_sha256: offering.reporting_profile.schema_sha256, + schema_dialect: offering.reporting_profile.schema_dialect, + schema_ref_policy: offering.reporting_profile.schema_ref_policy, + account_id: ACCOUNT_ID, + media_buy_ids: ['buy-1', 'buy-2'], + coverage: { + status: 'full', evaluated_at: now, + media_buy_ids: ['buy-1', 'buy-2'], fully_covered_media_buy_ids: ['buy-1', 'buy-2'], + partially_covered_media_buy_ids: [], unsupported_media_buy_ids: [], unknown_media_buy_ids: [], + package_ids: [], covered_package_ids: [], unsupported_package_ids: [], unknown_package_ids: [], limitations: [], + }, + period: { ...period, source_timezone: 'UTC' }, + finality: 'official', finality_basis: 'contractual_cutoff', + finality_policy_id: 'contractual-cutoff-v1', finalized_at: now, + observed_at: now, data_through: period.end, data_through_precision: 'exact', + row_count: rows.length, control_totals: controlTotals, + canonical_content_digest: structuredClone(CANONICAL_DIGEST), created_at: now, + }, + }; + await core.commitRevision(revision, lease); + const worker = await runtime.runWorker({ account_id: ACCOUNT_ID, maxIterations: 2 }); + if (worker.delivered !== 1 || worker.failed !== 0) { + throw new Error(`managed worker did not deliver exactly once: ${JSON.stringify(worker)}`); + } + const revisionView = await status('revision', { reporting_revision_id: revisionId }); + const materialization = revisionView.materializations?.[0]; + if (!materialization || materialization.status !== 'available' || !materialization.verification) { + throw new Error('authoritative status did not expose a verified available materialization'); + } + Object.assign(state, { + prepared: true, now, period, configuration, obligation, binding, lease, revision, + materialization, worker, + }); + return state; + } + + async function publishAdjustments() { + if (mode !== 'billing') throw new Error('adjustments require the Reconciled Billing fixture'); + await prepare(); + if (state.adjustments.length) return state.adjustments; + const specs = [ + { id: 'adjustment-billing-accepted-0001', number: 1, impressions: '1', reason: 'source_correction' }, + { id: 'adjustment-billing-disputed-0001', number: 2, impressions: '-1', reason: 'invalid_traffic' }, + ]; + for (const spec of specs) { + const rows = []; + const bytes = Buffer.from(api.jcs.canonicalize(rows), 'utf8'); + const withoutDigest = { + reporting_adjustment_id: spec.id, + adjusts_reporting_revision_id: state.revision.reporting_revision_id, + reason_code: spec.reason, + accounting_period: { start: state.period.start, end: state.period.end }, + control_total_deltas: [{ name: 'impressions', value: spec.impressions, value_type: 'integer', unit: 'impressions' }], + correction_observed_at: state.now, + created_at: state.now, + }; + const digest = api.ledger.reportingCanonicalAdjustmentSha256V1(withoutDigest); + const adjustment = { + reporting_adjustment_id: spec.id, + reporting_obligation_id: state.obligation.reporting_obligation_id, + adjusts_reporting_revision_id: state.revision.reporting_revision_id, + adjustmentNumber: spec.number, + manifest: { level: 'basic', objectRef: `${spec.id}-manifest`, sha256: 'f'.repeat(64), byteCount: 1 }, + sourcePublicationId: `${spec.id}-publication`, + binding: { algorithm: 'rfc8785_jcs_v1', sha256: sha256(bytes), byteCount: bytes.byteLength, rowCount: 0 }, + rows, observedAt: state.now, dataThrough: state.period.end, + sourceReadCutoffAt: state.now, createdAt: state.now, + wireAdjustment: { ...withoutDigest, canonical_adjustment_sha256: digest }, + }; + await core.commitAdjustment(adjustment, state.lease); + state.adjustments.push(adjustment); + } + return state.adjustments; + } + + async function controller(scenario, operation) { + const expected = mode === 'billing' + ? 'reliable_reporting_reconciled_billing_probe' + : 'reliable_reporting_managed_delivery_probe'; + if (scenario !== expected) throw new Error(`scenario ${scenario} is unavailable in ${mode} mode`); + if (operation === 'prepare') { + await prepare(); + return { success: true, simulated: { + destination_ref: state.binding.destination_ref, + reporting_obligation_id: state.obligation.reporting_obligation_id, + reporting_revision_id: state.revision.reporting_revision_id, + reporting_materialization_id: state.materialization.reporting_materialization_id, + canonical_content_digest: structuredClone(state.revision.wireRevision.canonical_content_digest), + } }; + } + if (mode === 'managed' && operation === 'suppress_readiness') { + await prepare(); + state.readinessNotificationSuppressed = true; + return { success: true, simulated: { readiness_notification_suppressed: true } }; + } + if (mode === 'managed' && operation === 'advance_within_retention') { + await prepare(); + const bytes = await runtime.readResource({ + account_id: ACCOUNT_ID, + resource_ref: state.materialization.resource.resource_ref, + }); + return { success: true, simulated: { + resource_readable: Boolean(bytes?.byteLength), + reporting_materialization_id: state.materialization.reporting_materialization_id, + } }; + } + if (mode === 'managed' && operation === 'revoke_access') { + await prepare(); + const started = Date.now(); + const revoked = await managed.revokeDestination({ + account_id: ACCOUNT_ID, + destination_ref: state.binding.destination_ref, + generation: state.binding.authorization_generation, + revoked_at: iso(started), + }); + const counts = await runtime.runWorker({ account_id: ACCOUNT_ID, maxIterations: 2 }); + const bytes = await runtime.readResource({ + account_id: ACCOUNT_ID, + resource_ref: state.materialization.resource.resource_ref, + }); + const retained = await status('revision', { reporting_revision_id: state.revision.reporting_revision_id }); + return { success: true, simulated: { + access_revoked: revoked && bytes === null && counts.revocationsCompleted === 1, + historical_metadata_retained: retained.materializations?.[0]?.reporting_materialization_id === + state.materialization.reporting_materialization_id, + revocation_elapsed_seconds: (Date.now() - started) / 1000, + } }; + } + if (mode === 'billing' && operation === 'publish_adjustment') { + const adjustments = await publishAdjustments(); + return { success: true, simulated: { + adjustments: adjustments.map(value => ({ + reporting_adjustment_id: value.reporting_adjustment_id, + adjusts_reporting_revision_id: value.adjusts_reporting_revision_id, + canonical_adjustment_sha256: value.wireAdjustment.canonical_adjustment_sha256, + })), + disputed_observed_adjustment_sha256: '0'.repeat(64), + } }; + } + throw new Error(`unsupported ${expected} operation: ${String(operation)}`); + } + + return { + accountId: ACCOUNT_ID, + consumerId: CONSUMER_ID, + mode, + core, + managed, + runtime, + provider, + state, + context, + controller, + prepare, + publishAdjustments, + status, + }; +} + +module.exports = { + ACCOUNT_ID, + CANONICAL_DIGEST, + CONSUMER_ID, + createManagedReportingFixture, + deliveryOffering, +}; diff --git a/scripts/ci/reporting_interop/ts_managed_reporting_http_probe.cjs b/scripts/ci/reporting_interop/ts_managed_reporting_http_probe.cjs new file mode 100644 index 000000000..7e43685b4 --- /dev/null +++ b/scripts/ci/reporting_interop/ts_managed_reporting_http_probe.cjs @@ -0,0 +1,262 @@ +#!/usr/bin/env node +'use strict'; + +/* Real-MCP focused preflight for managed/reconciled controller routes. */ + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { createRequire } = require('node:module'); +const { CANONICAL_DIGEST } = require('./ts_managed_reporting_fixture.cjs'); +const { callWithProbeAbort, createProbeAbort } = require('./ts_probe_abort.cjs'); + +function parseArgs(argv) { + const out = {}; + for (let index = 0; index < argv.length; index += 2) { + if (!argv[index]?.startsWith('--') || argv[index + 1] === undefined) throw new Error('invalid arguments'); + out[argv[index].slice(2)] = argv[index + 1]; + } + return out; +} + +async function main() { + const input = parseArgs(process.argv.slice(2)); + const lockPath = path.resolve(input['package-lock']); + const req = createRequire(path.join(path.dirname(lockPath), 'package.json')); + const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + const installed = req('@adcp/sdk/package.json'); + const entry = lock.packages?.['node_modules/@adcp/sdk']; + if (installed.version !== input['expected-version'] || entry?.integrity !== input['expected-integrity']) { + throw new Error('refusing to execute an unpinned SDK installation'); + } + const sdk = req('@adcp/sdk'); + const mode = input.mode; + if (!['managed', 'billing'].includes(mode)) throw new Error('mode must be managed or billing'); + const token = process.env.REPORTING_MANAGED_TOKEN; + if (!token) throw new Error('REPORTING_MANAGED_TOKEN is required'); + const agent = { + agent_uri: input.endpoint, + auth_token: token, + id: `managed-reporting-${mode}`, + name: `managed-reporting-${mode}`, + protocol: 'mcp', + }; + const probeAbort = createProbeAbort(`managed reporting ${mode} probe`); + const call = async (tool, params) => { + const raw = await callWithProbeAbort(probeAbort, signal => sdk.ProtocolClient.callTool( + agent, tool, params, { + adcpVersion: input['adcp-version'], + signal, + wireAdcpVersion: input['adcp-version'], + transport: { requestTimeoutMs: 10_000, maxResponseBytes: 4 * 1024 * 1024 }, + }, + )); + return sdk.unwrapProtocolResponse(raw, tool, 'mcp', { responseAdcpVersion: input['adcp-version'] }); + }; + const accountId = 'reporting_core_lab'; + const account = { account_id: accountId, sandbox: true }; + const scenario = mode === 'billing' + ? 'reliable_reporting_reconciled_billing_probe' + : 'reliable_reporting_managed_delivery_probe'; + const controller = operation => call('comply_test_controller', { + account, scenario, params: { operation }, + }); + const expectSelectorRejected = async selector => { + try { + await call('get_reporting_status', { account: selector, view: 'periods' }); + } catch (error) { + assert.match(String(error?.message || error), /authorized|FORBIDDEN/i); + return { selector, rejected: true }; + } + assert.fail(`unadmitted account selector was accepted: ${JSON.stringify(selector)}`); + }; + try { + const capabilities = await call('get_adcp_capabilities', {}); + assert.equal(capabilities.media_buy.reporting_delivery.managed_delivery, true); + assert.equal(capabilities.media_buy.reporting_delivery.reconciled_billing, mode === 'billing' ? true : undefined); + const selectorNegatives = [ + await expectSelectorRejected({ account_id: accountId }), + await expectSelectorRejected({ account_id: accountId, sandbox: false }), + ]; + const prepared = await controller('prepare'); + assert.equal(prepared.success, true); + let operations; + if (mode === 'managed') { + const suppressed = await controller('suppress_readiness'); + assert.equal(suppressed.simulated.readiness_notification_suppressed, true); + const status = await call('get_reporting_status', { + account, view: 'revision', reporting_revision_id: prepared.simulated.reporting_revision_id, + }); + assert.equal(status.materializations[0].reporting_materialization_id, + prepared.simulated.reporting_materialization_id); + assert.equal(status.materializations[0].status, 'available'); + assert.ok(status.materializations[0].verification.verified_at); + assert.ok(status.materializations[0].resource.expires_at); + const retention = await controller('advance_within_retention'); + assert.equal(retention.simulated.resource_readable, true); + const revoked = await controller('revoke_access'); + assert.equal(revoked.simulated.access_revoked, true); + assert.equal(revoked.simulated.historical_metadata_retained, true); + operations = { + prepare: prepared.simulated, + suppress_readiness: suppressed.simulated, + status: status.materializations[0].status, + retention: retention.simulated, + revocation: revoked.simulated, + }; + } else { + assert.deepEqual(prepared.simulated.canonical_content_digest, CANONICAL_DIGEST); + const baseline = await call('get_reporting_status', { account, view: 'periods' }); + assert.equal(baseline.periods[0].health, 'action_required'); + assert.equal(baseline.periods[0].issues[0].code, 'RECEIPT_REQUIRED'); + const common = { + reporting_obligation_id: prepared.simulated.reporting_obligation_id, + reporting_revision_id: prepared.simulated.reporting_revision_id, + reporting_materialization_id: prepared.simulated.reporting_materialization_id, + verification_profile: 'canonical_digest', + observed_row_count: 2, + observed_control_totals: [ + { name: 'impressions', value: '5', value_type: 'integer', unit: 'impressions' }, + { name: 'spend', value: '8.00', value_type: 'decimal', unit: 'USD' }, + ], + }; + const rejected = await call('sync_reporting_receipts', { + account, idempotency_key: '10000000-0000-4000-8000-000000000001', + receipts: [{ + ...common, reporting_receipt_id: 'rr-billing-official-receipt-0001', status: 'rejected', + rejection_codes: ['CANONICAL_DIGEST_MISMATCH'], observed_at: '2026-08-27T04:01:00Z', + }], + }); + assert.equal(rejected.results[0].result, 'recorded'); + const accepted = await call('sync_reporting_receipts', { + account, idempotency_key: '10000000-0000-4000-8000-000000000002', + receipts: [{ + ...common, reporting_receipt_id: 'rr-billing-official-repaired-0001', + supersedes_reporting_receipt_id: 'rr-billing-official-receipt-0001', status: 'accepted', + observed_canonical_content_digest: CANONICAL_DIGEST, observed_at: '2026-08-27T04:02:00Z', + }], + }); + assert.equal(accepted.results[0].result, 'recorded'); + const staleRevision = await call('sync_reporting_receipts', { + account, idempotency_key: '10000000-0000-4000-8000-000000000003', + receipts: [{ + ...common, reporting_receipt_id: 'rr-billing-official-stale-0001', + supersedes_reporting_receipt_id: 'rr-billing-official-repaired-0001', status: 'accepted', + observed_control_totals: [], observed_canonical_content_digest: CANONICAL_DIGEST, + observed_at: '2026-08-27T04:03:00Z', + }], + }); + assert.equal(staleRevision.results[0].result, 'failed'); + const acceptedStatus = await call('get_reporting_status', { account, view: 'periods' }); + assert.equal(acceptedStatus.periods[0].reconciliation_status, 'accepted'); + assert.equal(acceptedStatus.periods[0].health, 'complete'); + const history = await call('get_reporting_status', { + account, view: 'revision', reporting_revision_id: prepared.simulated.reporting_revision_id, + }); + assert.equal(history.receipts.length, 2); + + const published = await controller('publish_adjustment'); + assert.equal(published.simulated.adjustments.length, 2); + const reopened = await call('get_reporting_status', { account, view: 'periods' }); + assert.equal(reopened.periods[0].reconciliation_status, 'pending'); + assert.equal(reopened.periods[0].issues[0].code, 'ADJUSTMENT_RECEIPT_REQUIRED'); + const [acceptedAdjustment, disputedAdjustment] = published.simulated.adjustments; + const adjustmentReceipts = await call('sync_reporting_receipts', { + account, idempotency_key: '10000000-0000-4000-8000-000000000004', + adjustment_receipts: [ + { + reporting_receipt_id: 'rr-adjustment-accepted-receipt-01', + reporting_adjustment_id: acceptedAdjustment.reporting_adjustment_id, + adjusts_reporting_revision_id: prepared.simulated.reporting_revision_id, + status: 'accepted', observed_adjustment_sha256: acceptedAdjustment.canonical_adjustment_sha256, + observed_at: '2026-08-29T10:01:00Z', + }, + { + reporting_receipt_id: 'rr-adjustment-rejected-receipt-01', + reporting_adjustment_id: disputedAdjustment.reporting_adjustment_id, + adjusts_reporting_revision_id: prepared.simulated.reporting_revision_id, + status: 'rejected', + observed_adjustment_sha256: published.simulated.disputed_observed_adjustment_sha256, + rejection_codes: ['CANONICAL_DIGEST_MISMATCH'], observed_at: '2026-08-29T10:01:01Z', + }, + ], + }); + assert.deepEqual(adjustmentReceipts.results.map(value => value.result), ['recorded', 'recorded']); + const rejectedStatus = await call('get_reporting_status', { account, view: 'periods' }); + assert.equal(rejectedStatus.periods[0].reconciliation_status, 'rejected'); + assert.equal(rejectedStatus.periods[0].issues[0].code, 'ADJUSTMENT_RECEIPT_REJECTED'); + const repair = await call('sync_reporting_receipts', { + account, idempotency_key: '10000000-0000-4000-8000-000000000005', + adjustment_receipts: [{ + reporting_receipt_id: 'rr-adjustment-repaired-receipt-01', + supersedes_reporting_receipt_id: 'rr-adjustment-rejected-receipt-01', + reporting_adjustment_id: disputedAdjustment.reporting_adjustment_id, + adjusts_reporting_revision_id: prepared.simulated.reporting_revision_id, + status: 'accepted', observed_adjustment_sha256: disputedAdjustment.canonical_adjustment_sha256, + observed_at: '2026-08-29T10:02:00Z', + }], + }); + assert.equal(repair.results[0].result, 'recorded'); + const staleAdjustment = await call('sync_reporting_receipts', { + account, idempotency_key: '10000000-0000-4000-8000-000000000006', + adjustment_receipts: [{ + reporting_receipt_id: 'rr-adjustment-stale-receipt-01', + supersedes_reporting_receipt_id: 'rr-adjustment-repaired-receipt-01', + reporting_adjustment_id: disputedAdjustment.reporting_adjustment_id, + adjusts_reporting_revision_id: prepared.simulated.reporting_revision_id, + status: 'accepted', observed_adjustment_sha256: disputedAdjustment.canonical_adjustment_sha256, + observed_at: '2026-08-29T10:03:00Z', + }], + }); + assert.equal(staleAdjustment.results[0].result, 'failed'); + const restored = await call('get_reporting_status', { account, view: 'periods' }); + assert.equal(restored.periods[0].reconciliation_status, 'accepted'); + assert.equal(restored.periods[0].health, 'complete'); + assert.equal(restored.periods[0].adjustment_receipt_count, 3); + const repaired = await call('get_reporting_status', { + account, view: 'periods', changes_after: baseline.changes_checkpoint, + }); + assert.equal(repaired.adjustments.length, 2); + assert.equal(repaired.adjustment_receipts.length, 3); + operations = { + prepare: prepared.simulated, + baseline: { health: baseline.periods[0].health, issue: baseline.periods[0].issues[0].code }, + revision_receipts: [rejected.results[0].result, accepted.results[0].result, staleRevision.results[0].result], + revision_history: history.receipts.length, + adjustments: published.simulated.adjustments, + adjustment_receipts: adjustmentReceipts.results.map(value => value.adjustment_receipt.status), + adjustment_repair: [repair.results[0].result, staleAdjustment.results[0].result], + restored: { health: restored.periods[0].health, status: restored.periods[0].reconciliation_status }, + checkpoint_repair: { adjustments: repaired.adjustments.length, receipts: repaired.adjustment_receipts.length }, + }; + } + const result = { + kind: 'managed_reporting_controller_real_mcp_preflight', + status: 'passed', + blocking_acceptance: false, + mode, + package: { name: installed.name, version: installed.version }, + selector_negatives: selectorNegatives, + operations, + limitations: [ + 'focused_real_mcp_preflight_not_cli_storyboard_execution', + 'real_postgresql_real_clock', + ...(mode === 'managed' ? ['notification_suppression_not_signed_webhook_credit'] : []), + ], + }; + fs.writeFileSync(path.resolve(input.output), `${JSON.stringify(result, null, 2)}\n`); + process.stdout.write(`${JSON.stringify(result)}\n`); + } finally { + probeAbort.abort('probe settled'); + try { + await sdk.closeMCPConnections(); + } finally { + probeAbort.dispose(); + } + } +} + +main().catch(error => { + process.stderr.write(`${JSON.stringify({ kind: 'managed_reporting_http_probe_error', message: error.message, stack: error.stack })}\n`); + process.exitCode = 1; +}); diff --git a/scripts/ci/reporting_interop/ts_managed_reporting_probe.cjs b/scripts/ci/reporting_interop/ts_managed_reporting_probe.cjs new file mode 100644 index 000000000..9c55cf1a0 --- /dev/null +++ b/scripts/ci/reporting_interop/ts_managed_reporting_probe.cjs @@ -0,0 +1,342 @@ +#!/usr/bin/env node +'use strict'; + +/* Focused public-API preflight; this is not storyboard execution. */ + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { createRequire } = require('node:module'); +const { + ACCOUNT_ID, + CANONICAL_DIGEST, + createManagedReportingFixture, +} = require('./ts_managed_reporting_fixture.cjs'); + +function parseArgs(argv) { + const values = {}; + for (let index = 0; index < argv.length; index += 2) { + const flag = argv[index]; + const value = argv[index + 1]; + if (!flag?.startsWith('--') || value === undefined) throw new Error(`invalid argument near ${String(flag)}`); + values[flag.slice(2)] = value; + } + return values; +} + +function required(args, name) { + if (!args[name]) throw new Error(`missing --${name}`); + return args[name]; +} + +function loadPinned(args) { + const lockPath = path.resolve(required(args, 'package-lock')); + const req = createRequire(path.join(path.dirname(lockPath), 'package.json')); + const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + const entry = lock.packages?.['node_modules/@adcp/sdk']; + const installed = req('@adcp/sdk/package.json'); + assert.equal(installed.version, required(args, 'expected-version')); + assert.equal(entry?.version, installed.version); + assert.equal(entry?.integrity, required(args, 'expected-integrity')); + const sdk = req('@adcp/sdk'); + const ledger = req('@adcp/sdk/reporting/ledger'); + const schemas = req('@adcp/sdk/schemas'); + for (const name of [ + 'PostgresReportingLedgerStore', 'PostgresReportingManagedDeliveryStore', + 'createReportingManagedDeliveryRuntime', 'createSyncReportingReceiptsHandler', + 'reportingManagedDeliveryBindingV1', 'reportingCanonicalAdjustmentSha256V1', + ]) assert.ok(ledger[name], `missing public ledger export: ${name}`); + return { + installed, + sdk, + ledger, + schemas, + pg: req('pg'), + jcs: { canonicalize: sdk.canonicalize }, + }; +} + +function validate(api, tool, value) { + const convenience = api.schemas.TOOL_RESPONSE_SCHEMAS[tool].safeParse(value); + assert.equal(convenience.success, true, JSON.stringify(convenience.error?.issues || [])); + const canonical = api.schemas.getCanonicalToolValidator(tool, 'sync', { adcpVersion: '3.2.0-rc.4' }); + assert.equal(typeof canonical, 'function', `canonical ${tool} response validator unavailable`); + assert.equal(Boolean(canonical(value)), true, JSON.stringify(canonical.errors || [])); +} + +async function withFixture(api, bootstrap, mode, run) { + const schema = `adcp_interop_${mode}_${process.pid}_${Math.random().toString(16).slice(2)}`; + await bootstrap.query(`CREATE SCHEMA "${schema}"`); + const pool = new api.pg.Pool({ + connectionString: bootstrap.options.connectionString, + options: `-c search_path="${schema}"`, + max: 2, + }); + try { + const fixture = await createManagedReportingFixture(api, pool, { mode }); + return await run(fixture); + } finally { + await pool.end(); + await bootstrap.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`); + } +} + +function revisionReceipt(fixture, overrides = {}) { + return { + reporting_receipt_id: 'rr-billing-official-receipt-0001', + reporting_obligation_id: fixture.state.obligation.reporting_obligation_id, + reporting_revision_id: fixture.state.revision.reporting_revision_id, + reporting_materialization_id: fixture.state.materialization.reporting_materialization_id, + status: 'rejected', + verification_profile: 'canonical_digest', + observed_row_count: 2, + observed_control_totals: structuredClone(fixture.state.revision.wireRevision.control_totals), + rejection_codes: ['CANONICAL_DIGEST_MISMATCH'], + observed_at: fixture.state.now, + ...overrides, + }; +} + +async function managedPreflight(api, fixture) { + const prepared = await fixture.controller('reliable_reporting_managed_delivery_probe', 'prepare'); + assert.equal(prepared.success, true); + const suppressed = await fixture.controller('reliable_reporting_managed_delivery_probe', 'suppress_readiness'); + assert.equal(suppressed.simulated.readiness_notification_suppressed, true); + const status = await fixture.status('revision', { + reporting_revision_id: prepared.simulated.reporting_revision_id, + }); + validate(api, 'get_reporting_status', status); + assert.equal(status.materializations[0].reporting_materialization_id, + prepared.simulated.reporting_materialization_id); + assert.equal(status.materializations[0].status, 'available'); + assert.ok(status.materializations[0].verification?.verified_at); + assert.ok(status.materializations[0].resource?.expires_at); + const retention = await fixture.controller('reliable_reporting_managed_delivery_probe', 'advance_within_retention'); + assert.equal(retention.simulated.resource_readable, true); + assert.equal(retention.simulated.reporting_materialization_id, prepared.simulated.reporting_materialization_id); + const revoked = await fixture.controller('reliable_reporting_managed_delivery_probe', 'revoke_access'); + assert.equal(revoked.simulated.access_revoked, true); + assert.equal(revoked.simulated.historical_metadata_retained, true); + assert.equal(typeof revoked.simulated.revocation_elapsed_seconds, 'number'); + return { + prepared: prepared.simulated, + status: { + materialization_status: status.materializations[0].status, + verified: Boolean(status.materializations[0].verification), + adjustments_present: Array.isArray(status.adjustments), + }, + retention: retention.simulated, + revocation: revoked.simulated, + provider_events: structuredClone(fixture.provider.events), + }; +} + +async function billingPreflight(api, fixture) { + const prepared = await fixture.controller('reliable_reporting_reconciled_billing_probe', 'prepare'); + assert.equal(prepared.success, true); + assert.deepEqual(prepared.simulated.canonical_content_digest, CANONICAL_DIGEST); + const baseline = await fixture.status('periods'); + validate(api, 'get_reporting_status', baseline); + if (baseline.status === 'failed') { + await fixture.core.createSnapshot({ + account_id: ACCOUNT_ID, + consumer_id: fixture.consumerId, + view: 'periods', + }); + } + assert.ok(Array.isArray(baseline.revisions) && baseline.revisions.length > 0, + `periods baseline omitted revisions: ${JSON.stringify(baseline)}`); + assert.equal(baseline.revisions[0].reporting_revision_id, prepared.simulated.reporting_revision_id); + assert.equal(baseline.revisions[0].finality, 'official'); + assert.equal(baseline.periods[0].health, 'action_required'); + assert.ok(baseline.periods[0].issues.some(value => value.code === 'RECEIPT_REQUIRED')); + assert.ok(baseline.changes_checkpoint); + + const rejected = await fixture.runtime.syncReportingReceipts({ + account: { account_id: ACCOUNT_ID }, + idempotency_key: 'd09c6b4b-4ff4-4f46-8279-bcf3bb48b501', + receipts: [revisionReceipt(fixture)], + }, fixture.context); + validate(api, 'sync_reporting_receipts', rejected); + assert.equal(rejected.results[0].result, 'recorded'); + + const accepted = await fixture.runtime.syncReportingReceipts({ + account: { account_id: ACCOUNT_ID }, + idempotency_key: 'd09c6b4b-4ff4-4f46-8279-bcf3bb48b502', + receipts: [revisionReceipt(fixture, { + reporting_receipt_id: 'rr-billing-official-repaired-0001', + supersedes_reporting_receipt_id: 'rr-billing-official-receipt-0001', + status: 'accepted', + observed_canonical_content_digest: structuredClone(CANONICAL_DIGEST), + rejection_codes: undefined, + })], + }, fixture.context); + validate(api, 'sync_reporting_receipts', accepted); + assert.equal(accepted.results[0].result, 'recorded'); + + const staleRevision = await fixture.runtime.syncReportingReceipts({ + account: { account_id: ACCOUNT_ID }, + idempotency_key: 'd09c6b4b-4ff4-4f46-8279-bcf3bb48b503', + receipts: [revisionReceipt(fixture, { + reporting_receipt_id: 'rr-billing-official-stale-0001', + supersedes_reporting_receipt_id: 'rr-billing-official-repaired-0001', + status: 'accepted', + observed_control_totals: [], + observed_canonical_content_digest: structuredClone(CANONICAL_DIGEST), + rejection_codes: undefined, + })], + }, fixture.context); + validate(api, 'sync_reporting_receipts', staleRevision); + assert.equal(staleRevision.results[0].result, 'failed'); + const acceptedStatus = await fixture.status('periods'); + validate(api, 'get_reporting_status', acceptedStatus); + assert.equal(acceptedStatus.periods[0].reconciliation_status, 'accepted'); + assert.equal(acceptedStatus.periods[0].health, 'complete'); + const revisionHistory = await fixture.status('revision', { + reporting_revision_id: prepared.simulated.reporting_revision_id, + }); + assert.equal(revisionHistory.receipts.length, 2); + + const published = await fixture.controller('reliable_reporting_reconciled_billing_probe', 'publish_adjustment'); + assert.equal(published.success, true); + assert.equal(published.simulated.adjustments.length, 2); + const reopened = await fixture.status('periods'); + validate(api, 'get_reporting_status', reopened); + assert.equal(reopened.periods[0].reconciliation_status, 'pending'); + assert.ok(reopened.periods[0].issues.some(value => value.code === 'ADJUSTMENT_RECEIPT_REQUIRED')); + + const [acceptedAdjustment, disputedAdjustment] = published.simulated.adjustments; + const adjustmentReceipts = await fixture.runtime.syncReportingReceipts({ + account: { account_id: ACCOUNT_ID }, + idempotency_key: 'd09c6b4b-4ff4-4f46-8279-bcf3bb48b504', + adjustment_receipts: [ + { + reporting_receipt_id: 'rr-adjustment-accepted-receipt-01', + reporting_adjustment_id: acceptedAdjustment.reporting_adjustment_id, + adjusts_reporting_revision_id: prepared.simulated.reporting_revision_id, + status: 'accepted', + observed_adjustment_sha256: acceptedAdjustment.canonical_adjustment_sha256, + observed_at: fixture.state.now, + }, + { + reporting_receipt_id: 'rr-adjustment-rejected-receipt-01', + reporting_adjustment_id: disputedAdjustment.reporting_adjustment_id, + adjusts_reporting_revision_id: prepared.simulated.reporting_revision_id, + status: 'rejected', + observed_adjustment_sha256: published.simulated.disputed_observed_adjustment_sha256, + rejection_codes: ['CANONICAL_DIGEST_MISMATCH'], + observed_at: fixture.state.now, + }, + ], + }, fixture.context); + validate(api, 'sync_reporting_receipts', adjustmentReceipts); + assert.deepEqual(adjustmentReceipts.results.map(value => value.result), ['recorded', 'recorded']); + assert.equal(adjustmentReceipts.results[0].adjustment_receipt.status, 'accepted'); + assert.equal(adjustmentReceipts.results[1].adjustment_receipt.status, 'rejected'); + const rejectedStatus = await fixture.status('periods'); + assert.equal(rejectedStatus.periods[0].reconciliation_status, 'rejected'); + assert.ok(rejectedStatus.periods[0].issues.some(value => value.code === 'ADJUSTMENT_RECEIPT_REJECTED')); + + const repair = await fixture.runtime.syncReportingReceipts({ + account: { account_id: ACCOUNT_ID }, + idempotency_key: 'd09c6b4b-4ff4-4f46-8279-bcf3bb48b505', + adjustment_receipts: [{ + reporting_receipt_id: 'rr-adjustment-repaired-receipt-01', + supersedes_reporting_receipt_id: 'rr-adjustment-rejected-receipt-01', + reporting_adjustment_id: disputedAdjustment.reporting_adjustment_id, + adjusts_reporting_revision_id: prepared.simulated.reporting_revision_id, + status: 'accepted', + observed_adjustment_sha256: disputedAdjustment.canonical_adjustment_sha256, + observed_at: fixture.state.now, + }], + }, fixture.context); + assert.equal(repair.results[0].result, 'recorded'); + const staleAdjustment = await fixture.runtime.syncReportingReceipts({ + account: { account_id: ACCOUNT_ID }, + idempotency_key: 'd09c6b4b-4ff4-4f46-8279-bcf3bb48b506', + adjustment_receipts: [{ + reporting_receipt_id: 'rr-adjustment-stale-receipt-01', + supersedes_reporting_receipt_id: 'rr-adjustment-repaired-receipt-01', + reporting_adjustment_id: disputedAdjustment.reporting_adjustment_id, + adjusts_reporting_revision_id: prepared.simulated.reporting_revision_id, + status: 'accepted', + observed_adjustment_sha256: disputedAdjustment.canonical_adjustment_sha256, + observed_at: fixture.state.now, + }], + }, fixture.context); + assert.equal(staleAdjustment.results[0].result, 'failed'); + const restored = await fixture.status('periods'); + validate(api, 'get_reporting_status', restored); + assert.equal(restored.periods[0].reconciliation_status, 'accepted'); + assert.equal(restored.periods[0].health, 'complete'); + assert.equal(restored.periods[0].adjustment_receipt_count, 3); + const repaired = await fixture.status('periods', { changes_after: baseline.changes_checkpoint }); + validate(api, 'get_reporting_status', repaired); + assert.equal(repaired.revisions[0].reporting_revision_id, prepared.simulated.reporting_revision_id); + assert.equal(repaired.periods[0].adjustment_count, 2); + assert.equal(repaired.adjustments.length, 2); + assert.equal(repaired.adjustment_receipts.length, 3); + return { + prepared: prepared.simulated, + baseline: { + health: baseline.periods[0].health, + issue_codes: baseline.periods[0].issues.map(value => value.code), + checkpoint: baseline.changes_checkpoint, + }, + revision_receipts: { + rejected: rejected.results[0].result, + accepted_replacement: accepted.results[0].result, + stale_after_terminal: staleRevision.results[0].result, + history_count: revisionHistory.receipts.length, + }, + adjustments: published.simulated, + adjustment_receipts: { + initial: adjustmentReceipts.results.map(value => value.adjustment_receipt.status), + repaired: repair.results[0].result, + stale_after_terminal: staleAdjustment.results[0].result, + final_count: restored.periods[0].adjustment_receipt_count, + }, + final: { + reconciliation_status: restored.periods[0].reconciliation_status, + health: restored.periods[0].health, + checkpoint_repair_adjustments: repaired.adjustments.length, + checkpoint_repair_receipts: repaired.adjustment_receipts.length, + }, + }; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const api = loadPinned(args); + assert.equal(typeof api.sdk.canonicalize, 'function', 'missing public canonicalize export'); + const pgUrl = required(args, 'pg-url'); + const bootstrap = new api.pg.Pool({ connectionString: pgUrl, max: 1 }); + try { + const version = await bootstrap.query('SHOW server_version'); + const managed = await withFixture(api, bootstrap, 'managed', fixture => managedPreflight(api, fixture)); + const billing = await withFixture(api, bootstrap, 'billing', fixture => billingPreflight(api, fixture)); + const report = { + kind: 'managed_reporting_controller_preflight', + status: 'passed', + blocking_acceptance: false, + package: { name: api.installed.name, version: api.installed.version }, + runtime: { node: process.version, postgres: version.rows[0].server_version }, + operations: { managed, billing }, + limitations: [ + 'focused_public_controller_preflight_not_storyboard_execution', + 'real_postgresql_and_real_clock_not_controlled_clock_fixture', + 'notification_suppression_is_fixture_gate_not_signed_webhook_credit', + 'probe_scheduler_dst_unavailable_in_stock_rc45_producer', + ], + }; + fs.writeFileSync(path.resolve(required(args, 'output')), `${JSON.stringify(report, null, 2)}\n`); + process.stdout.write(`${JSON.stringify(report)}\n`); + } finally { + await bootstrap.end(); + } +} + +main().catch(error => { + process.stderr.write(`${JSON.stringify({ kind: 'managed_reporting_controller_preflight_failure', message: error.message, stack: error.stack })}\n`); + process.exitCode = 1; +}); diff --git a/scripts/ci/reporting_interop/ts_managed_reporting_server.cjs b/scripts/ci/reporting_interop/ts_managed_reporting_server.cjs new file mode 100644 index 000000000..ac0734b2e --- /dev/null +++ b/scripts/ci/reporting_interop/ts_managed_reporting_server.cjs @@ -0,0 +1,385 @@ +#!/usr/bin/env node +'use strict'; + +/* Official MCP wrapper for the public PostgreSQL managed controller fixture. */ + +const assert = require('node:assert/strict'); +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); +const { createRequire } = require('node:module'); +const { ACCOUNT_ID, CONSUMER_ID, createManagedReportingFixture } = require('./ts_managed_reporting_fixture.cjs'); +const { assertFixtureCapabilityVersion, wireAdcpVersion } = require('./ts_adcp_version.cjs'); +const { storyboardPrivateInputs } = require('./ts_private_inputs.cjs'); + +const STORYBOARD_OPERATOR = 'test.example'; + +function parseArgs(argv) { + const values = { host: '127.0.0.1', probe: false }; + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (token === '--probe') { values.probe = true; continue; } + if (!token.startsWith('--') || !argv[index + 1]) throw new Error(`invalid argument near ${token}`); + values[token.slice(2)] = argv[index + 1]; + index += 1; + } + return values; +} + +function required(args, name) { + if (!args[name]) throw new Error(`missing --${name}`); + return args[name]; +} + +function loadPinned(args) { + const lockPath = path.resolve(required(args, 'package-lock')); + const req = createRequire(path.join(path.dirname(lockPath), 'package.json')); + const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + const entry = lock.packages?.['node_modules/@adcp/sdk']; + const installed = req('@adcp/sdk/package.json'); + if (installed.version !== required(args, 'expected-version') || entry?.version !== installed.version || + entry?.integrity !== required(args, 'expected-integrity')) { + throw new Error('refusing to execute an unpinned SDK installation'); + } + const sdk = req('@adcp/sdk'); + return { + expectedIntegrity: required(args, 'expected-integrity'), + installed, + sdk, + ledger: req('@adcp/sdk/reporting/ledger'), + schemas: req('@adcp/sdk/schemas'), + server: req('@adcp/sdk/server'), + pg: req('pg'), + z: req('zod').z, + jcs: { canonicalize: sdk.canonicalize }, + }; +} + +function capabilities(fixture, adcpVersion) { + const wire = wireAdcpVersion(adcpVersion); + return { + adcp_version: wire, + adcp: { major_versions: [3], supported_versions: [wire], idempotency: { supported: false } }, + supported_protocols: ['media_buy'], + experimental_features: ['media_buy.reporting_delivery'], + compliance_testing: { + scenarios: [fixture.mode === 'billing' + ? 'reliable_reporting_reconciled_billing_probe' + : 'reliable_reporting_managed_delivery_probe'], + }, + account: { + require_operator_auth: true, + supported_billing: ['operator'], + supported_account_currency_modes: ['fixed'], + timezone: { mode: 'seller_fixed', fixed_timezone: 'UTC' }, + required_for_products: true, + sandbox: true, + }, + media_buy: { reporting_delivery: fixture.runtime.reportingDeliveryCapabilities }, + }; +} + +function mcpSuccess(api, value, summary) { + return { + content: [{ type: 'text', text: summary }], + structuredContent: { status: 'completed', ...api.server.toStructuredContent(value) }, + }; +} + +function accountContext(request, extra) { + const account = request?.account; + const accountId = account?.account_id; + const operator = account?.operator; + const selectedFixture = accountId === ACCOUNT_ID || operator === STORYBOARD_OPERATOR; + if (!selectedFixture || account?.sandbox !== true || + !extra?.authInfo?.extra?.accounts?.includes(ACCOUNT_ID)) { + throw new Error('caller is not authorized for the managed reporting sandbox account'); + } + return { account: { account_id: ACCOUNT_ID }, consumer_id: extra.authInfo.extra.consumer_id }; +} + +function validateAdvertisedCapabilities(api, fixture, adcpVersion, + advertised = capabilities(fixture, adcpVersion)) { + assertFixtureCapabilityVersion(advertised, adcpVersion); + const validator = api.schemas.getCanonicalToolValidator( + 'get_adcp_capabilities', 'sync', { adcpVersion }, + ); + assert.equal(typeof validator, 'function', 'canonical capability validator is unavailable'); + assert.equal(Boolean(validator({ ...advertised, status: 'completed' })), true, + JSON.stringify(validator.errors || [])); + return advertised; +} + +function buildReadyRecord(api, { adcpVersion, authToken, mode, port, startupProof, url }) { + return { + auth_binding_sha256: crypto.createHash('sha256').update(authToken).digest('hex'), + kind: 'managed_reporting_storyboard_server_ready', + mode, + node: { executable: fs.realpathSync(process.execPath), version: process.version }, + seller_package: { + adcp_version: adcpVersion, + integrity: api.expectedIntegrity, + name: api.installed.name, + version: api.installed.version, + }, + startup_proof_sha256: crypto.createHash('sha256').update(startupProof).digest('hex'), + pid: process.pid, + port, + url, + }; +} + +function createAgent(api, fixture, advertised) { + return () => { + const mcp = api.server.createTaskCapableServer('Managed reporting storyboard fixture', '1.0.0'); + mcp.registerTool('get_adcp_capabilities', { + description: 'Return exact managed reporting fixture capabilities.', + inputSchema: api.schemas.TOOL_INPUT_SHAPES.get_adcp_capabilities, + }, async () => api.server.legacyCapabilitiesResponse(advertised)); + mcp.registerTool('comply_test_controller', { + description: 'Drive declared managed reporting sandbox transitions.', + inputSchema: api.server.TOOL_INPUT_SHAPE, + }, async (input, extra) => { + accountContext(input, extra); + const result = await fixture.controller(input.scenario, input.params?.operation); + return api.server.toMcpResponse({ status: 'completed', ...result, context: input.context }); + }); + mcp.registerTool('get_reporting_status', { + description: 'Read authoritative managed reporting status.', + inputSchema: api.schemas.TOOL_INPUT_SHAPES.get_reporting_status, + }, async (request, extra) => { + const context = accountContext(request, extra); + const result = await fixture.runtime.getReportingStatus(request, context); + return mcpSuccess(api, result, 'get_reporting_status completed'); + }); + mcp.registerTool('get_media_buy_delivery', { + description: 'Read exact managed reporting revision rows.', + inputSchema: api.schemas.TOOL_INPUT_SHAPES.get_media_buy_delivery, + }, async (request, extra) => { + const context = accountContext(request, extra); + const result = await fixture.runtime.getMediaBuyDelivery(request, context); + return mcpSuccess(api, result, 'get_media_buy_delivery completed'); + }); + if (fixture.runtime.syncReportingReceipts) { + mcp.registerTool('sync_reporting_receipts', { + description: 'Record authenticated revision and adjustment receipts.', + inputSchema: api.schemas.TOOL_INPUT_SHAPES.sync_reporting_receipts, + }, async (request, extra) => { + const context = accountContext(request, extra); + const result = await fixture.runtime.syncReportingReceipts(request, context); + return mcpSuccess(api, result, 'sync_reporting_receipts completed'); + }); + } + return mcp; + }; +} + +function closeServer(server) { + return new Promise((resolve, reject) => { + let settled = false; + const done = error => { + if (settled) return; + settled = true; + if (error) reject(error); else resolve(); + }; + try { + const result = server.close(done); + if (result && typeof result.then === 'function') result.then(() => done(), done); + } catch (error) { + done(error); + } + }); +} + +function createResourceOwner(api, pgUrl, schema) { + let bootstrap; + let pool; + let schemaCreated = false; + let server; + let serverErrorListener; + let removeShutdownHandlers; + let cleanupPromise; + + return { + async acquire() { + bootstrap = new api.pg.Pool({ connectionString: pgUrl, max: 1 }); + await bootstrap.query(`CREATE SCHEMA "${schema}"`); + schemaCreated = true; + pool = new api.pg.Pool({ + connectionString: pgUrl, + options: `-c search_path="${schema}"`, + max: 4, + }); + return pool; + }, + ownServer(value, onError) { + server = value; + serverErrorListener = onError; + if (!server || typeof server.on !== 'function' || typeof server.removeListener !== 'function') { + throw new Error('managed reporting server does not expose owned error-listener methods'); + } + server.on('error', serverErrorListener); + }, + ownShutdownHandlers(value) { removeShutdownHandlers = value; }, + cleanup() { + if (cleanupPromise) return cleanupPromise; + cleanupPromise = (async () => { + const failures = []; + if (removeShutdownHandlers) { + try { removeShutdownHandlers(); } catch (error) { failures.push(error); } + } + // Server closure is always attempted first. Every other owned cleanup + // is still attempted if closure fails, while that uncertainty remains a + // blocking cleanup error rather than being hidden by later settlement. + if (server) { + try { await closeServer(server); } catch (error) { failures.push(error); } + if (serverErrorListener) { + try { server.removeListener('error', serverErrorListener); } + catch (error) { failures.push(error); } + } + } + if (pool) { + try { await pool.end(); } catch (error) { failures.push(error); } + } + if (schemaCreated && bootstrap) { + try { await bootstrap.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`); } + catch (error) { failures.push(error); } + } + if (bootstrap) { + try { await bootstrap.end(); } catch (error) { failures.push(error); } + } + if (failures.length) { + throw new AggregateError(failures, 'managed reporting resource cleanup failed'); + } + })(); + return cleanupPromise; + }, + }; +} + +async function failAfterCleanup(primary, owner) { + try { + await owner.cleanup(); + } catch (cleanup) { + const combined = new AggregateError( + [primary, cleanup], + 'managed reporting initialization failed and cleanup also failed', + ); + combined.cause = primary; + throw combined; + } + throw primary; +} + +function registerShutdownHandlers(close) { + const handlers = new Map(); + for (const signal of ['SIGINT', 'SIGTERM']) { + const handler = () => close().then(() => process.exit(0), () => process.exit(1)); + handlers.set(signal, handler); + process.once(signal, handler); + } + return () => { + for (const [signal, handler] of handlers) process.removeListener(signal, handler); + }; +} + +async function main(argv = process.argv.slice(2), dependencies = {}) { + const args = parseArgs(argv); + const api = (dependencies.loadPinned || loadPinned)(args); + const mode = required(args, 'mode'); + const pgUrl = required(args, 'pg-url'); + const adcpVersion = required(args, 'adcp-version'); + let normalInputs; + if (!args.probe) { + const port = Number(required(args, 'port')); + if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) throw new Error('invalid --port'); + normalInputs = { + port, + ...storyboardPrivateInputs(dependencies.environment || process.env), + readyFile: path.resolve(required(args, 'ready-file')), + }; + } + const schema = `adcp_interop_server_${mode}_${process.pid}`; + const owner = createResourceOwner(api, pgUrl, schema); + try { + const pool = await owner.acquire(); + const fixture = await (dependencies.createManagedReportingFixture || createManagedReportingFixture)( + api, pool, { mode }, + ); + const advertised = validateAdvertisedCapabilities(api, fixture, adcpVersion); + if (args.probe) { + (dependencies.writeOutput || (value => process.stdout.write(value)))(`${JSON.stringify({ + kind: 'managed_reporting_storyboard_server_probe', + status: 'passed', + blocking_acceptance: false, + mode, + package: { name: api.installed.name, version: api.installed.version }, + reporting_delivery: advertised.media_buy.reporting_delivery, + limitations: ['server_composition_probe_not_storyboard_execution', 'real_postgresql_real_clock'], + })}\n`); + await owner.cleanup(); + return; + } + + const { authToken, port, readyFile, startupProof } = normalInputs; + const authenticate = api.server.verifyApiKey({ + verify(token) { + if (token !== authToken) return null; + return { principal: 'reporting-managed-fixture-operator', extra: { accounts: [ACCOUNT_ID], consumer_id: CONSUMER_ID } }; + }, + }); + let resolveStartup; + let rejectStartup; + const startup = new Promise((resolve, reject) => { + resolveStartup = resolve; + rejectStartup = reject; + }); + const server = api.server.serve(createAgent(api, fixture, advertised), { + allowedHosts: [args.host, 'localhost', '127.0.0.1'], + authenticate, + path: '/mcp', + port, + onListening(url) { + try { + const ready = buildReadyRecord(api, { + adcpVersion, authToken, mode, port, startupProof, url, + }); + (dependencies.writeReadyFile || fs.writeFileSync)( + readyFile, `${JSON.stringify(ready)}\n`, { flag: 'wx', mode: 0o600 }, + ); + (dependencies.writeOutput || (value => process.stdout.write(value)))( + `${JSON.stringify(ready)}\n`, + ); + resolveStartup(ready); + } catch (error) { + rejectStartup(error); + } + }, + }); + owner.ownServer(server, error => rejectStartup(error)); + const close = () => owner.cleanup(); + owner.ownShutdownHandlers( + (dependencies.registerShutdownHandlers || registerShutdownHandlers)(close), + ); + const ready = await startup; + return { close, ready }; + } catch (error) { + return failAfterCleanup(error, owner); + } +} + +if (require.main === module) { + main().catch(error => { + process.stderr.write(`${JSON.stringify({ kind: 'managed_reporting_server_error', message: error.message, stack: error.stack })}\n`); + process.exitCode = 1; + }); +} + +module.exports = { + buildReadyRecord, + capabilities, + closeServer, + createResourceOwner, + main, + validateAdvertisedCapabilities, +}; diff --git a/scripts/ci/reporting_interop/ts_mcp_buyer.cjs b/scripts/ci/reporting_interop/ts_mcp_buyer.cjs new file mode 100644 index 000000000..0c31efbf1 --- /dev/null +++ b/scripts/ci/reporting_interop/ts_mcp_buyer.cjs @@ -0,0 +1,280 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const { createRequire } = require('node:module'); +const { callWithProbeAbort, createProbeAbort } = require('./ts_probe_abort.cjs'); + +function parseArgs(argv) { + const values = {}; + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token.startsWith('--')) throw new Error(`unexpected positional argument: ${token}`); + const key = token.slice(2); + const value = argv[index + 1]; + if (!value || value.startsWith('--')) throw new Error(`missing value for --${key}`); + if (Object.hasOwn(values, key)) throw new Error(`duplicate argument: --${key}`); + values[key] = value; + index += 1; + } + return values; +} + +function requireArg(args, name) { + if (!args[name]) throw new Error(`missing required argument: --${name}`); + return args[name]; +} + +function stable(value) { + if (Array.isArray(value)) return value.map(stable); + if (value && typeof value === 'object') { + return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])])); + } + return value; +} + +const SENSITIVE_KEY = /(authorization|credential|password|secret|token|private.?key|signed.?url)/i; + +function redact(value, secrets = []) { + if (Array.isArray(value)) return value.map(item => redact(item, secrets)); + if (value && typeof value === 'object') { + return Object.fromEntries(Object.entries(value).map(([key, item]) => [ + key, + SENSITIVE_KEY.test(key) ? '[REDACTED]' : redact(item, secrets), + ])); + } + if (typeof value !== 'string') return value; + let result = value; + for (const secret of secrets) { + if (secret) result = result.split(secret).join('[REDACTED]'); + } + result = result.replace(/(https?:\/\/[^\s?#]+)\?[^\s#]*/gi, '$1?[REDACTED]'); + result = result.replace(/\bBearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]'); + return result; +} + +function readInput(inputPath) { + const value = JSON.parse(fs.readFileSync(inputPath, 'utf8')); + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error('input must be a JSON object'); + } + const expectedPeriods = value.expected_periods ?? value.expectedPeriods; + if (!value.request || typeof value.request !== 'object') throw new Error('input.request is required'); + if (!Array.isArray(expectedPeriods)) throw new Error('input.expected_periods must be an array'); + if (!value.now || Number.isNaN(Date.parse(value.now))) throw new Error('input.now must be an RFC 3339 timestamp'); + const inspection = value.inspection || { mode: 'assert_not_called' }; + if (inspection.mode !== 'assert_not_called') { + throw new Error('only fail-closed inspection mode assert_not_called is implemented'); + } + const expectedOutcome = value.expected_outcome ?? value.expectedOutcome; + if (!expectedOutcome || typeof expectedOutcome !== 'object' || Array.isArray(expectedOutcome)) { + throw new Error('input.expected_outcome is required'); + } + return { expectedOutcome, expectedPeriods, inspection, value }; +} + +function assertExpectedOutcome(result, expected, inspectionCalls) { + if (typeof expected.definitive !== 'boolean') { + throw new Error('expected_outcome.definitive must be boolean'); + } + if (result.definitive !== expected.definitive) { + throw new Error(`semantic outcome mismatch: definitive=${String(result.definitive)}`); + } + if (expected.inspection_calls !== undefined && inspectionCalls !== expected.inspection_calls) { + throw new Error(`semantic outcome mismatch: inspection_calls=${inspectionCalls}`); + } + const obligations = Array.isArray(result.obligations) ? result.obligations : []; + if (expected.obligation_count !== undefined && obligations.length !== expected.obligation_count) { + throw new Error(`semantic outcome mismatch: obligation_count=${obligations.length}`); + } + const reasons = new Set(obligations.flatMap(item => Array.isArray(item.reasons) ? item.reasons : [])); + for (const reason of expected.reasons_absent || []) { + if (reasons.has(reason)) throw new Error(`semantic outcome mismatch: unexpected reason ${reason}`); + } + for (const reason of expected.reasons_present || []) { + if (!reasons.has(reason)) throw new Error(`semantic outcome mismatch: missing reason ${reason}`); + } +} + +function validateEndpoint(endpoint) { + const parsed = new URL(endpoint); + if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error('endpoint must use HTTP or HTTPS'); + if (parsed.username || parsed.password || parsed.search || parsed.hash) { + throw new Error('endpoint must not contain credentials, a query, or a fragment'); + } + return parsed.toString(); +} + +function verifyPackagePin(packageRequire, packageName, expectedVersion, expectedIntegrity, lockPath) { + const installed = packageRequire(`${packageName}/package.json`); + const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + const entry = lock.packages?.[`node_modules/${packageName}`]; + if (installed.version !== expectedVersion || entry?.version !== expectedVersion || + entry?.integrity !== expectedIntegrity) { + throw new Error('refusing to execute an unpinned SDK installation'); + } + return installed; +} + +function numericArg(args, name, fallback) { + if (!args[name]) return fallback; + const value = Number(args[name]); + if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`--${name} must be a positive integer`); + return value; +} + +function installRedactedConsole(secrets) { + for (const level of ['log', 'warn', 'error', 'info', 'debug']) { + console[level] = (...items) => { + const line = items.map(item => { + if (typeof item === 'string') return item; + try { + return JSON.stringify(item); + } catch { + return String(item); + } + }).join(' '); + process.stderr.write(`${redact(line, secrets)}\n`); + }; + } +} + +async function run() { + const args = parseArgs(process.argv.slice(2)); + const packageName = args.package || '@adcp/sdk'; + const expectedVersion = requireArg(args, 'expected-version'); + const expectedIntegrity = requireArg(args, 'expected-integrity'); + const lockPath = path.resolve(requireArg(args, 'package-lock')); + const adcpVersion = requireArg(args, 'adcp-version'); + const endpoint = validateEndpoint(requireArg(args, 'endpoint')); + const inputPath = path.resolve(requireArg(args, 'input')); + const authEnv = args['auth-env']; + const authToken = authEnv ? process.env[authEnv] : undefined; + if (authEnv && !authToken) throw new Error(`authentication environment variable is unset: ${authEnv}`); + const secrets = authToken ? [authToken] : []; + installRedactedConsole(secrets); + const packageRequire = createRequire(path.join(path.dirname(lockPath), 'package.json')); + const installed = verifyPackagePin( + packageRequire, packageName, expectedVersion, expectedIntegrity, lockPath, + ); + const { expectedOutcome, expectedPeriods, value: input } = readInput(inputPath); + const sdk = packageRequire(packageName); + for (const name of ['ProtocolClient', 'unwrapProtocolResponse', 'reconcileReporting', 'closeMCPConnections']) { + if (typeof sdk[name] !== 'function') throw new Error(`required public SDK export is unavailable: ${name}`); + } + + const transport = { + maxResponseBytes: numericArg(args, 'max-response-bytes', 16 * 1024 * 1024), + requestTimeoutMs: numericArg(args, 'request-timeout-ms', 30_000), + }; + const agent = { + agent_uri: endpoint, + auth_token: authToken, + id: 'reporting-interop-seller', + name: 'reporting-interop-seller', + protocol: 'mcp', + }; + const probeAbort = createProbeAbort('managed reconciliation buyer probe'); + const calls = []; + const call = async (toolName, params) => { + calls.push(toolName); + const raw = await callWithProbeAbort(probeAbort, signal => sdk.ProtocolClient.callTool( + agent, toolName, params, { adcpVersion, signal, transport, wireAdcpVersion: adcpVersion }, + )); + return sdk.unwrapProtocolResponse(raw, toolName, 'mcp', { responseAdcpVersion: adcpVersion }); + }; + const client = { + getMediaBuyDelivery: (params) => call('get_media_buy_delivery', params), + getReportingStatus: (params) => call('get_reporting_status', params), + syncReportingReceipts: (params) => call('sync_reporting_receipts', params), + syncReportingStatus: (params) => call('sync_reporting_status', params), + }; + let inspectionCalls = 0; + const inspect = async () => { + inspectionCalls += 1; + throw new Error('reporting artifact inspection was not declared for this scenario'); + }; + const options = { + client, + expectedPeriods, + inspect, + ledgerLimits: input.ledger_limits, + maxInspectionAttempts: input.max_inspection_attempts, + maxSnapshotRestarts: input.max_snapshot_restarts, + now: new Date(input.now), + operationsContact: input.operations_contact, + request: input.request, + }; + for (const key of Object.keys(options)) { + if (options[key] === undefined) delete options[key]; + } + + let result; + let executionError; + let cleanupError; + try { + result = await sdk.reconcileReporting(options); + } catch (error) { + executionError = error; + } finally { + probeAbort.abort('probe settled'); + try { + await sdk.closeMCPConnections(); + } catch (error) { + cleanupError = error; + } finally { + probeAbort.dispose(); + } + } + if (cleanupError) throw new Error(`MCP cleanup failed: ${redact(String(cleanupError.message || cleanupError), secrets)}`); + if (executionError) throw executionError; + assertExpectedOutcome(result, expectedOutcome, inspectionCalls); + + return { + adcp_version: adcpVersion, + call_counts: Object.fromEntries([...new Set(calls)].sort().map(name => [ + name, + calls.filter(value => value === name).length, + ])), + inspection_calls: inspectionCalls, + kind: 'reporting_interop_ts_mcp_buyer', + package: { + integrity: expectedIntegrity, + name: packageName, + version: installed.version, + }, + reconciliation: redact(result, secrets), + runtime: { node: process.version }, + schema_version: 1, + status: 'passed', + }; +} + +(async () => { + let report; + try { + report = await run(); + } catch (error) { + let failureSecrets = []; + try { + const args = parseArgs(process.argv.slice(2)); + const token = args['auth-env'] ? process.env[args['auth-env']] : undefined; + if (token) failureSecrets = [token]; + } catch { + // Argument parsing already failed; there is no trusted auth-env selector. + } + report = { + error: { + code: typeof error.code === 'string' ? error.code : null, + message: redact(String(error.message || error), failureSecrets), + name: error.name || 'Error', + }, + kind: 'reporting_interop_ts_mcp_buyer', + schema_version: 1, + status: 'failed', + }; + process.exitCode = 1; + } + process.stdout.write(`${JSON.stringify(stable(report), null, 2)}\n`); +})(); diff --git a/scripts/ci/reporting_interop/ts_package_probe.cjs b/scripts/ci/reporting_interop/ts_package_probe.cjs new file mode 100644 index 000000000..f8edc08c5 --- /dev/null +++ b/scripts/ci/reporting_interop/ts_package_probe.cjs @@ -0,0 +1,165 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const { createRequire } = require('node:module'); + +function parseArgs(argv) { + const values = {}; + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token.startsWith('--')) throw new Error(`unexpected positional argument: ${token}`); + const key = token.slice(2); + const value = argv[index + 1]; + if (!value || value.startsWith('--')) throw new Error(`missing value for --${key}`); + if (Object.hasOwn(values, key)) throw new Error(`duplicate argument: --${key}`); + values[key] = value; + index += 1; + } + return values; +} + +function requireArg(args, name) { + const value = args[name]; + if (!value) throw new Error(`missing required argument: --${name}`); + return value; +} + +function stable(value) { + if (Array.isArray(value)) return value.map(stable); + if (value && typeof value === 'object') { + return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])])); + } + return value; +} + +function fail(message, details = {}) { + const error = new Error(message); + error.details = details; + throw error; +} + +function packageLockEntry(lockPath, packageName) { + const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + const entry = lock.packages?.[`node_modules/${packageName}`]; + if (!entry) fail('package lock does not contain the SDK', { package: packageName }); + return entry; +} + +function registryVersionEntry(metadataPath, version) { + const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8')); + if (metadata.version === version) return metadata; + const entry = metadata.versions?.[version]; + if (!entry) fail('registry metadata does not contain the expected version', { version }); + return entry; +} + +function assertEqual(actual, expected, field) { + if (actual !== expected) fail(`installed package identity mismatch: ${field}`, { + actual: actual ?? null, + expected, + field, + }); +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + const packageName = args.package || '@adcp/sdk'; + const expectedVersion = requireArg(args, 'expected-version'); + const expectedIntegrity = requireArg(args, 'expected-integrity'); + const expectedTarball = requireArg(args, 'expected-tarball'); + const expectedGitHead = requireArg(args, 'expected-git-head'); + const expectedShasum = requireArg(args, 'expected-shasum'); + const lockPath = path.resolve(requireArg(args, 'package-lock')); + const metadataPath = path.resolve(requireArg(args, 'registry-metadata')); + const packageRequire = createRequire(path.join(path.dirname(lockPath), 'package.json')); + + const packageFile = packageRequire.resolve(`${packageName}/package.json`); + const packageRoot = fs.realpathSync(path.dirname(packageFile)); + const installed = packageRequire(packageFile); + const rootEntry = fs.realpathSync(packageRequire.resolve(packageName)); + if (!rootEntry.startsWith(`${packageRoot}${path.sep}`)) { + fail('public package entry resolved outside the installed package root'); + } + + const lockEntry = packageLockEntry(lockPath, packageName); + const registryEntry = registryVersionEntry(metadataPath, expectedVersion); + assertEqual(installed.name, packageName, 'package.name'); + assertEqual(installed.version, expectedVersion, 'package.version'); + assertEqual(lockEntry.version, expectedVersion, 'lock.version'); + assertEqual(lockEntry.integrity, expectedIntegrity, 'lock.integrity'); + assertEqual(lockEntry.resolved, expectedTarball, 'lock.resolved'); + assertEqual(registryEntry.name, packageName, 'registry.name'); + assertEqual(registryEntry.version, expectedVersion, 'registry.version'); + assertEqual(registryEntry.gitHead, expectedGitHead, 'registry.gitHead'); + assertEqual(registryEntry.dist?.integrity, expectedIntegrity, 'registry.dist.integrity'); + assertEqual(registryEntry.dist?.tarball, expectedTarball, 'registry.dist.tarball'); + assertEqual(registryEntry.dist?.shasum, expectedShasum, 'registry.dist.shasum'); + + const sdk = packageRequire(packageName); + const requiredRootExports = [ + 'ProtocolClient', + 'unwrapProtocolResponse', + 'reconcileReporting', + 'closeMCPConnections', + ]; + const rootExports = Object.fromEntries( + requiredRootExports.map(name => [name, typeof sdk[name]]), + ); + for (const [name, type] of Object.entries(rootExports)) { + if (type !== 'function') fail('required public SDK export is unavailable', { export: name, type }); + } + + const declaredSubpaths = [ + '.', + './client', + './schemas', + './server', + './reporting/ledger', + './reporting/service', + ]; + const publicSubpaths = Object.fromEntries( + declaredSubpaths.map(subpath => [subpath, Object.hasOwn(installed.exports || {}, subpath)]), + ); + if (Object.values(publicSubpaths).some(value => !value)) { + fail('required public package subpath is not declared', { publicSubpaths }); + } + + return { + identity: { + git_head: expectedGitHead, + integrity: expectedIntegrity, + package: packageName, + shasum: expectedShasum, + tarball: expectedTarball, + version: expectedVersion, + }, + kind: 'reporting_interop_ts_package_probe', + public_subpaths: publicSubpaths, + root_exports: rootExports, + runtime: { + arch: process.arch, + node: process.version, + platform: process.platform, + }, + schema_version: 1, + status: 'passed', + }; +} + +try { + process.stdout.write(`${JSON.stringify(stable(main()), null, 2)}\n`); +} catch (error) { + process.stdout.write(`${JSON.stringify(stable({ + error: { + details: error.details || {}, + message: String(error.message || error), + name: error.name || 'Error', + }, + kind: 'reporting_interop_ts_package_probe', + schema_version: 1, + status: 'failed', + }), null, 2)}\n`); + process.exitCode = 1; +} diff --git a/scripts/ci/reporting_interop/ts_primary_facade_gap.cjs b/scripts/ci/reporting_interop/ts_primary_facade_gap.cjs new file mode 100644 index 000000000..f13c342fc --- /dev/null +++ b/scripts/ci/reporting_interop/ts_primary_facade_gap.cjs @@ -0,0 +1,125 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const { createRequire } = require('node:module'); + +function parseArgs(argv) { + const values = { 'expect-gap': false }; + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (token === '--expect-gap') { + values['expect-gap'] = true; + continue; + } + if (!token.startsWith('--')) throw new Error(`unexpected positional argument: ${token}`); + const value = argv[index + 1]; + if (!value || value.startsWith('--')) throw new Error(`missing value for ${token}`); + values[token.slice(2)] = value; + index += 1; + } + return values; +} + +function requireArg(args, name) { + if (!args[name]) throw new Error(`missing required argument: --${name}`); + return args[name]; +} + +function stable(value) { + if (Array.isArray(value)) return value.map(stable); + if (value && typeof value === 'object') { + return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])])); + } + return value; +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + const expectedVersion = requireArg(args, 'expected-version'); + const expectedIntegrity = requireArg(args, 'expected-integrity'); + const lockPath = path.resolve(requireArg(args, 'package-lock')); + const adcpVersion = requireArg(args, 'adcp-version'); + const packageName = args.package || '@adcp/sdk'; + const packageRequire = createRequire(path.join(path.dirname(lockPath), 'package.json')); + const installed = packageRequire(`${packageName}/package.json`); + const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + const lockEntry = lock.packages?.[`node_modules/${packageName}`]; + if (installed.version !== expectedVersion || lockEntry?.version !== expectedVersion || + lockEntry?.integrity !== expectedIntegrity) { + throw new Error('refusing to reproduce against an unpinned SDK installation'); + } + + const sdk = packageRequire(packageName); + const requiredLowerLevel = ['ProtocolClient', 'unwrapProtocolResponse', 'reconcileReporting']; + const lowerLevel = Object.fromEntries(requiredLowerLevel.map(name => [name, typeof sdk[name]])); + if (Object.values(lowerLevel).some(type => type !== 'function')) { + throw new Error('required public lower-level reporting primitives are unavailable'); + } + if (typeof sdk.ADCPMultiAgentClient !== 'function') { + throw new Error('public ADCPMultiAgentClient constructor is unavailable'); + } + + // Construction is deliberately offline. The reproduction inspects only the + // documented primary facade and never calls the reserved endpoint. + const client = new sdk.ADCPMultiAgentClient([{ + agent_uri: 'http://127.0.0.1:9/mcp', + id: 'installed-package-surface-probe', + name: 'installed-package-surface-probe', + protocol: 'mcp', + }], { adcpVersion, wireAdcpVersion: adcpVersion }); + if (client.getAdcpVersion() !== adcpVersion) { + throw new Error('primary facade did not retain the exact AdCP version pin'); + } + const agent = client.agent('installed-package-surface-probe'); + const requiredFacade = [ + 'getReportingStatus', + 'syncReportingReceipts', + 'syncReportingStatus', + ]; + const facade = Object.fromEntries(requiredFacade.map(name => [name, typeof agent[name]])); + facade.reconcileReporting = typeof agent.reconcileReporting; + facade.reporting = typeof agent.reporting; + const missing = requiredFacade.filter(name => facade[name] !== 'function'); + const gapReproduced = missing.length > 0; + + return { + adcp_version: adcpVersion, + expected_gap_mode: Boolean(args['expect-gap']), + gap_reproduced: gapReproduced, + kind: 'reporting_interop_ts_primary_facade_gap', + lower_level_public_exports: lowerLevel, + missing_primary_facade_methods: missing, + package: { + integrity: expectedIntegrity, + name: packageName, + version: installed.version, + }, + primary_facade: facade, + primary_facade_adcp_version: client.getAdcpVersion(), + runtime: { node: process.version }, + schema_version: 1, + semantic_lane_complete: false, + surface_available: !gapReproduced, + status: gapReproduced ? 'surface_gap_reproduced' : 'surface_available_nonsemantic', + limitation: 'offline method-presence introspection does not execute reporting semantics', + }; +} + +try { + const args = parseArgs(process.argv.slice(2)); + const result = main(); + process.stdout.write(`${JSON.stringify(stable(result), null, 2)}\n`); + if (result.gap_reproduced && !args['expect-gap']) process.exitCode = 1; + if (!result.gap_reproduced && args['expect-gap']) process.exitCode = 2; +} catch (error) { + process.stdout.write(`${JSON.stringify(stable({ + error: { message: String(error.message || error), name: error.name || 'Error' }, + kind: 'reporting_interop_ts_primary_facade_gap', + schema_version: 1, + semantic_lane_complete: false, + status: 'failed', + }), null, 2)}\n`); + process.exitCode = 2; +} diff --git a/scripts/ci/reporting_interop/ts_private_inputs.cjs b/scripts/ci/reporting_interop/ts_private_inputs.cjs new file mode 100644 index 000000000..9667778d4 --- /dev/null +++ b/scripts/ci/reporting_interop/ts_private_inputs.cjs @@ -0,0 +1,66 @@ +'use strict'; + +/* + * Private per-run inputs for the TypeScript reporting fixtures. + * + * Environment variables are not a secrecy boundary against the same user or + * root. This seam only avoids exposing credentials and startup proofs in the + * process argument vector and in ordinary command/error logging. + */ + +const CORE_PRIVATE_INPUT_ENV = Object.freeze({ + tokenA: 'ADCP_INTEROP_TS_CORE_AUTH_TOKEN_A', + tokenB: 'ADCP_INTEROP_TS_CORE_AUTH_TOKEN_B', + startupProof: 'ADCP_INTEROP_TS_CORE_STARTUP_PROOF', +}); + +const STORYBOARD_PRIVATE_INPUT_ENV = Object.freeze({ + authToken: 'ADCP_INTEROP_TS_STORYBOARD_AUTH_TOKEN', + startupProof: 'ADCP_INTEROP_TS_STORYBOARD_STARTUP_PROOF', +}); + +function requiredPrivateInput(environment, variable, label) { + const value = environment?.[variable]; + if (typeof value !== 'string' || value.length < 32) { + throw new Error(`${label} must be supplied as a strong per-run private input`); + } + return value; +} + +function corePrivateInputs(environment = process.env) { + const tokenA = requiredPrivateInput( + environment, CORE_PRIVATE_INPUT_ENV.tokenA, 'Core seller account A token', + ); + const tokenB = requiredPrivateInput( + environment, CORE_PRIVATE_INPUT_ENV.tokenB, 'Core seller account B token', + ); + const startupProof = requiredPrivateInput( + environment, CORE_PRIVATE_INPUT_ENV.startupProof, 'Core seller startup proof', + ); + if (tokenA === tokenB) { + throw new Error('Core seller authentication requires distinct per-run tokens'); + } + return { tokenA, tokenB, startupProof }; +} + +function storyboardPrivateInputs(environment = process.env) { + return { + authToken: requiredPrivateInput( + environment, + STORYBOARD_PRIVATE_INPUT_ENV.authToken, + 'Storyboard seller authentication token', + ), + startupProof: requiredPrivateInput( + environment, + STORYBOARD_PRIVATE_INPUT_ENV.startupProof, + 'Storyboard seller startup proof', + ), + }; +} + +module.exports = { + CORE_PRIVATE_INPUT_ENV, + STORYBOARD_PRIVATE_INPUT_ENV, + corePrivateInputs, + storyboardPrivateInputs, +}; diff --git a/scripts/ci/reporting_interop/ts_probe_abort.cjs b/scripts/ci/reporting_interop/ts_probe_abort.cjs new file mode 100644 index 000000000..054d8844d --- /dev/null +++ b/scripts/ci/reporting_interop/ts_probe_abort.cjs @@ -0,0 +1,42 @@ +'use strict'; + +/* Shared cancellation boundary for installed-package HTTP probes. + * + * A request timeout is only a deadline. It does not prove that sibling calls + * or signal-interrupted work stopped. This helper supplies one AbortSignal to + * every call in a probe and aborts the full probe on the first call failure or + * process signal. The owner still performs its normal connection drain and + * process-group cleanup; cancellation is not treated as cleanup proof. + */ + +function createProbeAbort(label) { + const controller = new AbortController(); + const abort = reason => { + if (controller.signal.aborted) return; + const error = reason instanceof Error ? reason : new Error(`${label}: ${String(reason)}`); + controller.abort(error); + }; + const onSigint = () => abort('received SIGINT'); + const onSigterm = () => abort('received SIGTERM'); + process.once('SIGINT', onSigint); + process.once('SIGTERM', onSigterm); + return { + abort, + signal: controller.signal, + dispose() { + process.removeListener('SIGINT', onSigint); + process.removeListener('SIGTERM', onSigterm); + }, + }; +} + +async function callWithProbeAbort(boundary, invoke) { + try { + return await invoke(boundary.signal); + } catch (error) { + boundary.abort(error); + throw error; + } +} + +module.exports = { callWithProbeAbort, createProbeAbort }; diff --git a/scripts/ci/reporting_interop/ts_reporting_controller_probe.cjs b/scripts/ci/reporting_interop/ts_reporting_controller_probe.cjs new file mode 100644 index 000000000..329d95544 --- /dev/null +++ b/scripts/ci/reporting_interop/ts_reporting_controller_probe.cjs @@ -0,0 +1,417 @@ +#!/usr/bin/env node +'use strict'; + +/* + * Focused preflight for the rc.45 Reliable Reporting Core controller adapter. + * + * This is deliberately not a storyboard runner. It drives the installed + * comply_test_controller dispatcher and the installed public producer/store + * interfaces, then records which literal controller operations are ready for + * CLI execution. It never rewrites status output or moves the PostgreSQL clock. + */ + +const fs = require('node:fs'); +const path = require('node:path'); +const { createHash } = require('node:crypto'); +const { createRequire } = require('node:module'); + +const PERIOD_START = '2026-08-01T00:00:00.000Z'; +const PERIOD_END = '2026-08-01T01:00:00.000Z'; +const EXPECTED_AT = '2026-08-01T02:00:00.000Z'; +const RECOVERY_DEADLINE = '2026-08-01T04:00:00.000Z'; +const WORKER_NOW = '2026-08-01T04:05:00.000Z'; +const ACCOUNTS = ['reporting_core_lab', 'reporting_core_nonempty_lab']; + +function parseArgs(argv) { + const out = {}; + for (let index = 0; index < argv.length; index += 2) { + const flag = argv[index]; + const value = argv[index + 1]; + if (!flag?.startsWith('--') || value === undefined) throw new Error(`invalid argument near ${String(flag)}`); + out[flag.slice(2)] = value; + } + return out; +} + +function required(args, name) { + const value = args[name]; + if (!value) throw new Error(`missing --${name}`); + return value; +} + +function loadPinned(args) { + const lockPath = path.resolve(required(args, 'package-lock')); + const req = createRequire(path.join(path.dirname(lockPath), 'package.json')); + const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + const entry = lock.packages?.['node_modules/@adcp/sdk']; + const installed = req('@adcp/sdk/package.json'); + if (installed.version !== required(args, 'expected-version') || + entry?.version !== installed.version || entry?.integrity !== required(args, 'expected-integrity')) { + throw new Error('refusing to execute an unpinned SDK installation'); + } + return { + installed, + ledger: req('@adcp/sdk/reporting/ledger'), + source: req('@adcp/sdk/reporting/source'), + server: req('@adcp/sdk/server'), + schemas: req('@adcp/sdk/schemas'), + pg: req('pg'), + }; +} + +function contract() { + return { + report_definition_id: 'reporting-core-hourly-v1', + reportDefinitionUri: 'https://contracts.example/reporting-core-hourly-v1.json', + reportDefinitionSha256: 'a'.repeat(64), + reportingProfile: 'reporting-core-hourly-v1', + schemaVersion: '1', + schemaUri: 'https://contracts.example/reporting-core-hourly-rows-v1.json', + schemaSha256: 'b'.repeat(64), + schemaDialect: 'https://json-schema.org/draft/2020-12/schema', + schemaRefPolicy: 'local_fragment_only', + mappingId: 'reporting-core-hourly-v1', + mappingVersion: '1', + mappingSha256: 'c'.repeat(64), + }; +} + +function offering(source) { + const value = structuredClone(source.redactedReportingSourceOfferingV1); + value.offeringId = 'reporting-core-hourly'; + value.publicationNamespace = 'reporting-source:interop-core-controller'; + value.applicability.productIds = ['reporting-core-product']; + value.contract = contract(); + value.grain = 'source_hour'; + value.windowing.minimumWindow = 'PT1H'; + value.windowing.maximumWindow = 'PT1H'; + value.cadence.alignment = 'source_timezone'; + value.sourceExecution.maximumWindowDaysPerRequest = 1; + return source.ReportingSourceOfferingV1Schema.parse(value); +} + +function configuration(accountId, selectedOffering, source) { + const request = source.redactedReportingSourceRequestV1(); + return { + account: { account_id: accountId }, + sourceScope: { connection: accountId, region: 'test' }, + delivery_config_id: `delivery-${accountId}`, + delivery_config_version: 1, + offeringId: selectedOffering.offeringId, + report_definition_id: selectedOffering.contract.report_definition_id, + feedPurpose: 'analytics', + requiredFinality: 'snapshot', + requestedMetrics: ['impressions'], + requestedDimensions: ['media_buy_id'], + constituents: [{ + constituentId: `constituent-${accountId}`, + constituentKind: 'media_buy', + productId: 'reporting-core-product', + mediaBuyId: `media-buy-${accountId}`, + productBinding: { + owner: 'caller', + bindingId: `binding-${accountId}`, + bindingVersion: 1, + bindingSha256: 'd'.repeat(64), + bindingKind: 'media_buy_product', + productId: 'reporting-core-product', + mediaBuyId: `media-buy-${accountId}`, + }, + }], + mediaBuyIds: [`media-buy-${accountId}`], + sourceTimezone: 'UTC', + schedule: { + anchor: PERIOD_START, + periodMilliseconds: 3_600_000, + deliverySlaMilliseconds: 3_600_000, + recoveryWindowMilliseconds: 7_200_000, + periodDuration: 'PT1H', + alignment: 'source_timezone', + periodTimezone: 'UTC', + deliverySlaDuration: 'PT1H', + }, + sourceSettings: structuredClone(request.sourceSettings), + contract: structuredClone(selectedOffering.contract), + }; +} + +function exactRows() { + return [ + { + period_start: PERIOD_START, + period_end: PERIOD_END, + impressions: 2, + dimensions: { media_buy_id: 'media-buy-core-001', package_id: 'package-core-001', country: 'US' }, + metrics: { impressions: 2, clicks: 1 }, + }, + { + period_start: PERIOD_START, + period_end: PERIOD_END, + impressions: 3, + dimensions: { media_buy_id: 'media-buy-core-002', package_id: 'package-core-002', country: 'CA' }, + metrics: { impressions: 3, clicks: 0 }, + }, + ]; +} + +function fixedInstallStore(store) { + return new Proxy(store, { + get(target, property) { + if (property === 'putConfiguration') { + return value => target.putConfiguration({ ...value, installedAt: PERIOD_START }); + } + const selected = Reflect.get(target, property, target); + return typeof selected === 'function' ? selected.bind(target) : selected; + }, + }); +} + +async function createCoreController(api, pool) { + await pool.query(api.ledger.REPORTING_LEDGER_MIGRATION); + await pool.query(api.ledger.REPORTING_LEDGER_FINALITY_WRITER_FENCE_MIGRATION); + const store = new api.ledger.PostgresReportingLedgerStore(pool, { acknowledgeIsolatedDatabase: true }); + const selectedOffering = offering(api.source); + const sourceState = new Map(); + const sourceDiagnostic = {}; + const objects = new Map(); + const executor = { + capabilities: api.source.reportingSourceCapabilitiesV1([selectedOffering]), + async execute(request) { + const accountId = request.account.account_id; + const state = sourceState.get(accountId); + if (!state) throw new Error(`source was not armed for ${accountId}`); + const rows = structuredClone(state.rows); + const bytes = Buffer.from(rows.map(row => JSON.stringify(row)).join('\n') + (rows.length ? '\n' : '')); + const sha256 = createHash('sha256').update(bytes).digest('hex'); + const executionDigest = createHash('sha256') + .update(request.identity.sourceExecutionKey) + .digest('hex'); + const objectRef = `controller-${executionDigest}`; + const objectGeneration = `sha256-${sha256}`; + const dataThrough = request.period.end; + const metrics = new Map(selectedOffering.metrics.map(metric => [metric.name, metric])); + const metricAvailability = request.coverage.constituents.flatMap(constituent => + request.requestedMetrics.map(name => { + const metric = metrics.get(name); + if (!metric) throw new Error(`requested metric is absent from offering: ${name}`); + return { + constituentId: constituent.constituentId, + metric: name, + semanticContractId: metric.semanticContractId, + semanticContractVersion: metric.semanticContractVersion, + semanticContractSha256: metric.semanticContractSha256, + status: rows.length === 0 ? 'explicit_zero' : 'present', + dataThrough, + }; + })); + const coverageConstituents = request.coverage.constituents.map(constituent => ({ + ...structuredClone(constituent), + status: rows.length === 0 ? 'explicit_zero' : 'present', + dataThrough, + })); + const object = { + ordinal: 0, + objectRef, + objectGeneration, + mediaType: 'application/x-ndjson', + compression: 'none', + sha256, + byteCount: bytes.byteLength, + rowCount: rows.length, + }; + const impressions = rows.reduce((sum, row) => sum + Number(row.impressions ?? 0), 0); + let built; + try { + built = api.source.buildReportingSourceManifestV1({ + level: 'basic', + request, + stagedCommitRef: `manifest-${executionDigest}`, + objects: [object], + completeness: { terminal: true, rowsComplete: true, requestedGroupsComplete: true }, + controlTotals: [{ name: 'impressions', value: String(impressions), valueType: 'integer', unit: 'impressions' }], + metricAvailability, + coverage: { status: 'full', constituents: coverageConstituents }, + observedAt: dataThrough, + dataThrough, + finalityEvidence: { owner: 'adapter', basis: 'provisional_observation', observedAt: dataThrough }, + acquiredAt: dataThrough, + explicitZero: rows.length === 0, + ...(rows.length ? { eventTimeRange: { start: request.period.start, end: dataThrough } } : {}), + warnings: [], + }); + } catch (error) { + sourceDiagnostic.error = error; + throw error; + } + objects.set(objectRef, { bytes, generation: objectGeneration, request: structuredClone(request) }); + return { + ok: true, + response: api.source.completedReportingSourceResponseV1({ request, manifest: built.reference }), + manifestBytes: built.manifestBytes, + }; + }, + async read(input) { + const object = objects.get(input.objectRef); + if (!object || object.generation !== input.objectGeneration || + object.request.account.account_id !== input.account.account_id || + object.request.reporting_obligation_id !== input.reporting_obligation_id || + object.bytes.byteLength > input.maxBytes) { + throw new Error('staged reporting object does not match its public read binding'); + } + return Uint8Array.from(object.bytes); + }, + }; + const producer = api.ledger.createReportingProducer({ + store: fixedInstallStore(store), + source: executor, + offerings: [selectedOffering], + contact: { name: 'Reporting interoperability controller' }, + }); + const installed = new Set(); + const diagnostic = {}; + + async function prepare(accountId) { + if (!installed.has(accountId)) { + await producer.installConfiguration(configuration(accountId, selectedOffering, api.source)); + installed.add(accountId); + } + const obligations = await producer.planObligations('2026-08-01T01:05:00.000Z', { + account_id: accountId, + maxObligations: 1, + }); + const obligation = obligations[0] ?? (await store.listObligations(accountId))[0]; + if (!obligation) throw new Error(`producer did not plan the first obligation for ${accountId}`); + if (obligation.period.start !== PERIOD_START || obligation.period.end !== PERIOD_END || + obligation.expectedAt !== EXPECTED_AT || obligation.recoveryDeadlineAt !== RECOVERY_DEADLINE) { + throw new Error('producer planned unexpected Core period boundaries'); + } + return obligation; + } + + async function publish(accountId, rows) { + const obligation = await prepare(accountId); + sourceState.set(accountId, { rows: structuredClone(rows) }); + const worker = await producer.runWorker({ + account_id: accountId, + maxIterations: 2, + now: () => new Date(WORKER_NOW), + }); + if (worker.revisionsCommitted !== 1 || worker.failed !== 0) { + const issues = await store.listIssues(obligation.reporting_obligation_id); + const sourceError = sourceDiagnostic.error + ? String(sourceDiagnostic.error?.stack || sourceDiagnostic.error) + : 'none'; + throw new Error(`public producer did not commit one revision: ${JSON.stringify(worker)}; issues=${JSON.stringify(issues)}; source=${sourceError}`); + } + const revisions = await store.listRevisions(obligation.reporting_obligation_id); + const revision = revisions.at(-1); + if (!revision) throw new Error('committed revision was not readable through the public store'); + return { obligation, revision, worker }; + } + + const factory = { + scenarios: ['reporting_core_lifecycle_probe'], + createStore(input) { + const accountId = input?.account?.account_id; + if (!ACCOUNTS.includes(accountId) || input?.account?.sandbox !== true) { + throw new api.server.TestControllerError('FORBIDDEN', 'Controller requires a persisted sandbox account'); + } + return { + async reportingCoreLifecycleProbe(params) { + try { + const operation = params.operation; + if (operation === 'prepare') { + const obligation = await prepare(accountId); + return { success: true, simulated: { + delivery_config_id: obligation.delivery_config_id, + expected_at: obligation.expectedAt, + period: structuredClone(obligation.period), + recovery_deadline: obligation.recoveryDeadlineAt, + reporting_obligation_id: obligation.reporting_obligation_id, + } }; + } + if (operation === 'publish_zero_row' || operation === 'publish_nonempty') { + const rows = operation === 'publish_zero_row' ? [] : exactRows(); + const { obligation, revision } = await publish(accountId, rows); + return { success: true, simulated: { + reporting_obligation_id: obligation.reporting_obligation_id, + reporting_revision_id: revision.reporting_revision_id, + revision_content_sha256: revision.binding.sha256, + row_count: revision.binding.rowCount, + } }; + } + throw new api.server.TestControllerError('INVALID_PARAMS', `Unsupported Core operation: ${String(operation)}`); + } catch (error) { + diagnostic.error = error; + throw error; + } + }, + }; + }, + }; + return { diagnostic, factory, store }; +} + +async function invoke(api, factory, diagnostic, accountId, operation) { + const request = { + account: { account_id: accountId, sandbox: true }, + scenario: 'reporting_core_lifecycle_probe', + params: { operation }, + context: { correlation_id: `controller-preflight--${accountId}--${operation}` }, + }; + const response = await api.server.handleTestControllerRequest(factory, request); + const parsed = api.schemas.TOOL_RESPONSE_SCHEMAS.comply_test_controller.safeParse(response); + if (!parsed.success) throw new Error(`controller response failed installed schema: ${JSON.stringify(parsed.error.issues)}`); + if (response.success !== true) { + const detail = diagnostic.error ? `; cause=${String(diagnostic.error?.stack || diagnostic.error)}` : ''; + throw new Error(`controller operation failed: ${JSON.stringify(response)}${detail}`); + } + return response; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const api = loadPinned(args); + const pool = new api.pg.Pool({ connectionString: required(args, 'database-url'), max: 1 }); + try { + const { diagnostic, factory, store } = await createCoreController(api, pool); + const waiting = await invoke(api, factory, diagnostic, ACCOUNTS[0], 'prepare'); + const zero = await invoke(api, factory, diagnostic, ACCOUNTS[0], 'publish_zero_row'); + const nonemptyPrepared = await invoke(api, factory, diagnostic, ACCOUNTS[1], 'prepare'); + const nonempty = await invoke(api, factory, diagnostic, ACCOUNTS[1], 'publish_nonempty'); + const zeroRevision = await store.getRevision(zero.simulated.reporting_revision_id, ACCOUNTS[0]); + const nonemptyRevision = await store.getRevision(nonempty.simulated.reporting_revision_id, ACCOUNTS[1]); + const result = { + kind: 'reporting_storyboard_controller_preflight', + status: 'passed', + blocking_acceptance: false, + package: { name: api.installed.name, version: api.installed.version }, + operations: { + prepare_waiting_obligation: { controller: waiting, public_revision_count: 0 }, + publish_zero_row: { controller: zero, stored_row_count: zeroRevision?.binding?.rowCount }, + prepare_nonempty_core: { controller: nonemptyPrepared, public_revision_count: 0 }, + publish_nonempty_core: { + controller: nonempty, + stored_row_count: nonemptyRevision?.binding?.rowCount, + literal_storyboard_identity_match: + nonempty.simulated.reporting_revision_id === 'reporting-revision.ecc62efa00946aa1e2788ad9' && + nonempty.simulated.revision_content_sha256 === '5199a776b3a99915f084e16c921b2e501fcedd949017236d51a2303c5c2f5cd1', + }, + }, + limitations: [ + 'focused_public_controller_preflight_not_storyboard_execution', + 'postgres_status_clock_not_injectable', + 'consumer_status_and_optional_tiers_not_exercised', + ], + }; + fs.writeFileSync(required(args, 'output'), `${JSON.stringify(result, null, 2)}\n`); + process.stdout.write(`${JSON.stringify(result)}\n`); + } finally { + await pool.end(); + } +} + +main().catch(error => { + process.stderr.write(`${JSON.stringify({ kind: 'controller_preflight_error', message: String(error?.message || error) })}\n`); + process.exitCode = 1; +}); diff --git a/scripts/ci/reporting_interop/ts_storyboard_inventory.cjs b/scripts/ci/reporting_interop/ts_storyboard_inventory.cjs new file mode 100644 index 000000000..31564dbe9 --- /dev/null +++ b/scripts/ci/reporting_interop/ts_storyboard_inventory.cjs @@ -0,0 +1,227 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const crypto = require('node:crypto'); +const { spawnSync } = require('node:child_process'); +const { createRequire } = require('node:module'); + +const REPORTING_STORYBOARDS = new Set([ + 'reliable_reporting_managed_delivery', + 'reliable_reporting_reconciled_billing', + 'reporting_consumer_status', + 'reporting_core', + 'reporting_core_declaration', +]); + +const STORYBOARD_FILES = { + reliable_reporting_managed_delivery: 'reliable-reporting-managed-delivery.yaml', + reliable_reporting_reconciled_billing: 'reliable-reporting-reconciled-billing.yaml', + reporting_consumer_status: 'reporting-consumer-status.yaml', + reporting_core: 'reporting-core.yaml', + reporting_core_declaration: 'reporting-core-declaration.yaml', +}; + +function parseArgs(argv) { + const values = {}; + for (let index = 0; index < argv.length; index += 2) { + const key = argv[index]; + const value = argv[index + 1]; + if (!key?.startsWith('--') || !value) throw new Error(`invalid argument: ${key || ''}`); + values[key.slice(2)] = value; + } + return values; +} + +function requireArg(args, name) { + if (!args[name]) throw new Error(`missing required argument: --${name}`); + return args[name]; +} + +function stable(value) { + if (Array.isArray(value)) return value.map(stable); + if (value && typeof value === 'object') { + return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])])); + } + return value; +} + +function sha256File(file) { + return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); +} + +function storyboardRequirements(document) { + const scenarios = new Map(); + function walk(value) { + if (Array.isArray(value)) { + for (const item of value) walk(item); + return; + } + if (!value || typeof value !== 'object') return; + if (typeof value.comply_scenario === 'string') { + if (!scenarios.has(value.comply_scenario)) scenarios.set(value.comply_scenario, new Set()); + } + if (typeof value.scenario === 'string' && value.scenario.includes('reporting')) { + if (!scenarios.has(value.scenario)) scenarios.set(value.scenario, new Set()); + if (typeof value.params?.operation === 'string') { + scenarios.get(value.scenario).add(value.params.operation); + } + } + for (const item of Object.values(value)) walk(item); + } + walk(document); + return { + capabilities: Array.isArray(document.requires_all_capabilities) + ? document.requires_all_capabilities.map(item => ({ equals: item.equals, path: item.path })) + : [], + controller_required: Array.isArray(document.requires) && document.requires.includes('controller'), + controller_scenarios: [...scenarios.entries()] + .map(([scenario, operations]) => ({ operations: [...operations].sort(), scenario })) + .sort((left, right) => left.scenario.localeCompare(right.scenario)), + required_tools: Array.isArray(document.required_tools) ? [...document.required_tools] : [], + }; +} + +function storyboardSteps(document) { + const steps = []; + for (const phase of document.phases || []) { + for (const step of phase.steps || []) { + steps.push({ + comply_scenario: step.comply_scenario ?? null, + controller_operation: step.sample_request?.params?.operation ?? null, + id: step.id, + phase_id: phase.id, + stateful: step.stateful === true, + task: step.task, + title: step.title, + }); + } + } + return steps; +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + const packageName = args.package || '@adcp/sdk'; + const expectedVersion = requireArg(args, 'expected-version'); + const expectedIntegrity = requireArg(args, 'expected-integrity'); + const lockPath = path.resolve(requireArg(args, 'package-lock')); + const packageRequire = createRequire(path.join(path.dirname(lockPath), 'package.json')); + const installed = packageRequire(`${packageName}/package.json`); + const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + const lockEntry = lock.packages?.[`node_modules/${packageName}`]; + if (installed.version !== expectedVersion || lockEntry?.version !== expectedVersion || + lockEntry?.integrity !== expectedIntegrity) { + throw new Error('refusing to inventory an unpinned SDK installation'); + } + const packageFile = packageRequire.resolve(`${packageName}/package.json`); + const sdk = packageRequire(packageName); + const yaml = packageRequire('yaml'); + const testing = packageRequire(`${packageName}/testing`); + const server = packageRequire(`${packageName}/server`); + if (!server.CONTROLLER_SCENARIOS || typeof server.CONTROLLER_SCENARIOS !== 'object' || + Array.isArray(server.CONTROLLER_SCENARIOS)) { + throw new Error('public CONTROLLER_SCENARIOS export is missing or malformed'); + } + const builtInScenarios = Object.values(server.CONTROLLER_SCENARIOS); + if (builtInScenarios.length === 0) { + throw new Error('public CONTROLLER_SCENARIOS export contains no declared scenarios'); + } + const storyboardRoot = path.join( + path.dirname(packageFile), 'compliance', 'cache', sdk.ADCP_VERSION, 'universal'); + const cliRelative = typeof installed.bin === 'string' ? installed.bin : installed.bin?.adcp; + if (!cliRelative) throw new Error('installed package has no public adcp CLI bin'); + const cli = path.resolve(path.dirname(packageFile), cliRelative); + const completed = spawnSync(process.execPath, [cli, 'storyboard', 'list', '--json'], { + cwd: path.dirname(lockPath), + encoding: 'utf8', + env: { ...process.env, NO_COLOR: '1' }, + maxBuffer: 8 * 1024 * 1024, + timeout: 60_000, + }); + if (completed.error) throw completed.error; + if (completed.status !== 0) throw new Error(`storyboard list failed: ${completed.stderr}`); + const document = JSON.parse(completed.stdout); + const rows = Array.isArray(document.storyboards) ? document.storyboards : []; + const reporting = rows + .filter(row => REPORTING_STORYBOARDS.has(row.id)) + .map(row => { + const file = STORYBOARD_FILES[row.id]; + if (!file) throw new Error(`missing storyboard file mapping for ${row.id}`); + const source = path.join(storyboardRoot, file); + const sourceReal = fs.realpathSync(source); + const packageRoot = fs.realpathSync(path.dirname(packageFile)); + if (!sourceReal.startsWith(`${packageRoot}${path.sep}`) || !fs.statSync(sourceReal).isFile()) { + throw new Error(`storyboard member escaped the installed package: ${row.id}`); + } + const parsed = yaml.parse(fs.readFileSync(source, 'utf8')); + const requirements = storyboardRequirements(parsed); + const steps = storyboardSteps(parsed); + return { + id: row.id, + requirements, + source: path.relative(path.dirname(packageFile), source), + source_member_identity: { + package_integrity: expectedIntegrity, + package_version: installed.version, + path: path.relative(path.dirname(packageFile), source), + sha256: sha256File(sourceReal), + }, + stateful_step_count: row.stateful_step_count, + step_count: row.step_count, + steps, + }; + }) + .sort((left, right) => left.id.localeCompare(right.id)); + if (reporting.length !== REPORTING_STORYBOARDS.size || + new Set(reporting.map(row => row.id)).size !== REPORTING_STORYBOARDS.size) { + throw new Error('installed CLI did not expose the exact five reporting storyboards'); + } + for (const row of reporting) { + if (!Number.isSafeInteger(row.step_count) || !Number.isSafeInteger(row.stateful_step_count) || + row.step_count < row.stateful_step_count || row.stateful_step_count < 0) { + throw new Error(`invalid storyboard counts for ${row.id}`); + } + if (row.steps.length !== row.step_count || + row.steps.filter(step => step.stateful).length !== row.stateful_step_count || + new Set(row.steps.map(step => step.id)).size !== row.step_count) { + throw new Error(`storyboard step identities drifted for ${row.id}`); + } + } + return { + inventory: reporting, + controller_public_surface: { + createComplyController: typeof testing.createComplyController, + handleTestControllerRequest: typeof server.handleTestControllerRequest, + registerTestController: typeof server.registerTestController, + TOOL_INPUT_SHAPE: typeof server.TOOL_INPUT_SHAPE, + toMcpResponse: typeof server.toMcpResponse, + built_in_scenarios: builtInScenarios.sort(), + reporting_core_flat_store_hook: 'reportingCoreLifecycleProbe', + note: 'Reliable-reporting probe adapters are not turnkey exports; custom controller adapters must drive real seller state. Compliance cache paths are bound package members, not independent semver API promises.', + }, + kind: 'reporting_interop_ts_storyboard_inventory', + package: { integrity: expectedIntegrity, name: packageName, version: installed.version }, + runtime: { node: process.version }, + schema_version: 2, + status: 'passed', + totals: { + stateful_steps: reporting.reduce((sum, row) => sum + row.stateful_step_count, 0), + steps: reporting.reduce((sum, row) => sum + row.step_count, 0), + storyboards: reporting.length, + }, + }; +} + +try { + process.stdout.write(`${JSON.stringify(stable(main()), null, 2)}\n`); +} catch (error) { + process.stdout.write(`${JSON.stringify(stable({ + error: { message: String(error.message || error), name: error.name || 'Error' }, + kind: 'reporting_interop_ts_storyboard_inventory', + schema_version: 1, + status: 'failed', + }), null, 2)}\n`); + process.exitCode = 1; +} diff --git a/scripts/ci/reporting_interop_inputs.json b/scripts/ci/reporting_interop_inputs.json new file mode 100644 index 000000000..c9de3c12b --- /dev/null +++ b/scripts/ci/reporting_interop_inputs.json @@ -0,0 +1,121 @@ +{ + "schema_version": 1, + "issue": "https://github.com/adcontextprotocol/adcp-client-python/issues/1199", + "phase": "preparation", + "protocols": { + "stable": { + "version": "3.2.0-rc.3", + "target_commit": "71f9cd5414454e94ccfef87ce25777ead6fad228", + "release_url": "https://github.com/adcontextprotocol/adcp/releases/tag/v3.2.0-rc.3", + "source_url": "https://github.com/adcontextprotocol/adcp/releases/download/v3.2.0-rc.3/3.2.0-rc.3.tgz", + "sha256": "1cd940f76516b43327cbfa6c17c0befd4c093ca85b53a81a4062d38bb522d831", + "checksum_url": "https://github.com/adcontextprotocol/adcp/releases/download/v3.2.0-rc.3/3.2.0-rc.3.tgz.sha256", + "checksum_sha256": "4c1dac976d456cefd113132b6351f255b5d0bffff558593905877c9f382471ec", + "signature_url": "https://github.com/adcontextprotocol/adcp/releases/download/v3.2.0-rc.3/3.2.0-rc.3.tgz.sig", + "signature_sha256": "4a81a4d64f03d6f8edbd920815cd4fdb227e1767a5d62d1beb74a1cc9a9154c9", + "certificate_url": "https://github.com/adcontextprotocol/adcp/releases/download/v3.2.0-rc.3/3.2.0-rc.3.tgz.crt", + "certificate_sha256": "ffe8ddae6432277cc195ce76d37829c5e041a76d6f192269c75695c61c50a168" + }, + "candidate": { + "version": "3.2.0-rc.4", + "target_commit": "94976657c8456e5ad6de55d9793a883542a4fc5f", + "release_url": "https://github.com/adcontextprotocol/adcp/releases/tag/v3.2.0-rc.4", + "source_url": "https://github.com/adcontextprotocol/adcp/releases/download/v3.2.0-rc.4/3.2.0-rc.4.tgz", + "sha256": "773bee016d279345d6fae91a0ce684a22ca9b81c2edd2cdb9a71dbfea65954d2", + "checksum_url": "https://github.com/adcontextprotocol/adcp/releases/download/v3.2.0-rc.4/3.2.0-rc.4.tgz.sha256", + "checksum_sha256": "5f8ec3f329675a2a95f3301b743a57325d044f3e38165ea41093ade0a0e3c6ba", + "signature_url": "https://github.com/adcontextprotocol/adcp/releases/download/v3.2.0-rc.4/3.2.0-rc.4.tgz.sig", + "signature_sha256": "493f5e9b77f8fde5b666bb0e0cbfc605f842ed311bd5e3670e82538cd91610e2", + "certificate_url": "https://github.com/adcontextprotocol/adcp/releases/download/v3.2.0-rc.4/3.2.0-rc.4.tgz.crt", + "certificate_sha256": "9f7ce3a9bdfed7996cacb3b57cc2a3bd2f0a1e9bcd0aa3ca92772387e64bbe41" + } + }, + "artifacts": { + "python": { + "stable": null, + "candidate": null + }, + "typescript": { + "stable": { + "package": "@adcp/sdk", + "version": "14.0.0-rc.41", + "registry": "https://registry.npmjs.org/", + "tarball_url": "https://registry.npmjs.org/@adcp/sdk/-/sdk-14.0.0-rc.41.tgz", + "integrity": "sha512-Qfs+ujKBpIjf7m9jcuzxvN/XCXUssxhR5xCvxCq4oqMR3aMJjnuTgzjxltPtv+A6LOxwmX1ia8sMRdhVSehdBg==", + "shasum": "394b62123fb31c02aeec92f7f12874142fac9a09" + }, + "candidate": null + } + }, + "pending": { + "python_stable": "Pin the accepted installed B2.4/#1172-capable artifact; do not substitute the current main checkout or PyPI 8.0.0b15 baseline.", + "python_candidate": "Pin the accepted rc.4 replacement plus #1172 artifact only after its immutable build and acceptance identity exist.", + "typescript_candidate": { + "repository": "adcontextprotocol/adcp-client", + "release_pr": 2979, + "source_merge_commit": "3d8c3dfec6d0b325d602964e067703f05408d76f", + "proposed_version": "14.0.0-rc.42", + "forbidden_substitute": "14.0.0-rc.41" + } + }, + "execution": { + "postgresql_version": null, + "os_release_sha256": null, + "database_encoding": "UTF8", + "database_collation": "C", + "seller_entrypoint": null, + "seller_entrypoint_sha256": null, + "buyer_entrypoint": null, + "buyer_entrypoint_sha256": null, + "fixture_identity": { + "account_id": "interop-account-001", + "consumer_id": "interop-consumer-001", + "media_buy_id": "interop-media-buy-001", + "delivery_config_id": "interop-delivery-config-001", + "reporting_obligation_id": "interop-obligation-2026-09-01", + "reporting_revision_id": "interop-revision-2026-09-01-r1", + "reporting_materialization_id": "interop-materialization-2026-09-01-r1" + } + }, + "cells": [ + { + "id": "python-stable__typescript-stable", + "python": "stable", + "typescript": "stable", + "protocol": "stable", + "required": true + }, + { + "id": "python-stable__typescript-candidate", + "python": "stable", + "typescript": "candidate", + "protocol": "stable", + "required": true + }, + { + "id": "python-candidate__typescript-stable", + "python": "candidate", + "typescript": "stable", + "protocol": "stable", + "required": true + }, + { + "id": "python-candidate__typescript-candidate", + "python": "candidate", + "typescript": "candidate", + "protocol": "candidate", + "required": true + } + ], + "baseline_inputs": { + "python_registry": { + "package": "adcp", + "version": "8.0.0b15", + "registry": "https://pypi.org/", + "wheel_url": "https://files.pythonhosted.org/packages/8b/63/cf3d7b2885126771009f6da3786ba564fcae7e910568c93f06cf672d2f5e/adcp-8.0.0b15-py3-none-any.whl", + "wheel_sha256": "608636a4fe774bc4846b5bb0eab6367d0b8e69f6f371aa6085845d9f8329f48f", + "protocol": "3.2.0-rc.3", + "acceptance": false + } + } +} diff --git a/scripts/ci/reporting_interop_matrix.py b/scripts/ci/reporting_interop_matrix.py new file mode 100644 index 000000000..84f09618f --- /dev/null +++ b/scripts/ci/reporting_interop_matrix.py @@ -0,0 +1,1107 @@ +#!/usr/bin/env python3 +"""Fail-closed input and evidence tooling for reporting interop issue #1199. + +This module intentionally does not turn a checkout, a dist-tag, or a release PR +into an acceptance input. It validates the fixed four-cell topology, verifies +the immutable inputs that are currently available, and monitors the corrected +TypeScript release without mutating the pin manifest. +""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import re +import shutil +import subprocess +import sys +import tempfile +import urllib.error +import urllib.request +from collections.abc import Iterable, Mapping +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +DEFAULT_MANIFEST = Path(__file__).with_name("reporting_interop_inputs.json") +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +NPM_REGISTRY = "https://registry.npmjs.org/" +MUTABLE_VERSIONS = {"latest", "next", "rc", "beta", "alpha", "canary", "main", "master", "head"} +HEX_40 = re.compile(r"^[0-9a-f]{40}$") +HEX_64 = re.compile(r"^[0-9a-f]{64}$") +EXACT_VERSION = re.compile(r"^[0-9]+(?:\.[0-9]+){2}(?:(?:[-+.]?)[A-Za-z0-9][A-Za-z0-9.-]*)?$") +EXPECTED_CELLS = ( + ("python-stable__typescript-stable", "stable", "stable", "stable"), + ("python-stable__typescript-candidate", "stable", "candidate", "stable"), + ("python-candidate__typescript-stable", "candidate", "stable", "stable"), + ("python-candidate__typescript-candidate", "candidate", "candidate", "candidate"), +) +REQUIRED_ASSERTIONS = ( + "installed_python_artifact", + "installed_typescript_artifact", + "real_mcp_http", + "initial_state_empty", + "deterministic_fixture_identity", + "managed_delivery_status", + "reconciled_billing_status", + "semantic_reconcile_reporting", + "receipt_recorded", + "exact_revision_read", + "signed_webhook_rejected_first_attempt", + "signed_webhook_retry_succeeded", + "signed_webhook_replay_idempotent", + "account_activity_retry_visible", + "final_state_expected", +) + + +class InputError(ValueError): + """An input cannot qualify for the requested mode.""" + + +@dataclass(frozen=True) +class Validation: + ready: bool + missing: tuple[str, ...] + + +def _load(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise InputError(f"cannot read manifest {path}: {error}") from error + if not isinstance(value, dict): + raise InputError("manifest root must be an object") + return value + + +def _object(value: Any, field: str) -> Mapping[str, Any]: + if not isinstance(value, dict): + raise InputError(f"{field} must be an object") + return value + + +def _text(value: Any, field: str) -> str: + if not isinstance(value, str) or not value: + raise InputError(f"{field} must be a non-empty string") + return value + + +def _exact_version(value: Any, field: str) -> str: + version = _text(value, field) + if version.lower() in MUTABLE_VERSIONS or not EXACT_VERSION.fullmatch(version): + raise InputError(f"{field} must be an exact package version, got {version!r}") + return version + + +def _https(value: Any, field: str) -> str: + url = _text(value, field) + if not url.startswith("https://") or any( + token in url.lower() for token in ("/latest", "/main/", "/master/") + ): + raise InputError(f"{field} must be an immutable HTTPS URL, got {url!r}") + return url + + +def _digest(value: Any, field: str, pattern: re.Pattern[str]) -> str: + digest = _text(value, field).lower() + if not pattern.fullmatch(digest): + raise InputError(f"{field} has the wrong digest shape") + return digest + + +def _validate_protocol(name: str, raw: Any) -> None: + item = _object(raw, f"protocols.{name}") + version = _exact_version(item.get("version"), f"protocols.{name}.version") + if version != ("3.2.0-rc.3" if name == "stable" else "3.2.0-rc.4"): + raise InputError(f"protocols.{name}.version is not the controlling protocol pin") + _digest(item.get("target_commit"), f"protocols.{name}.target_commit", HEX_40) + for key in ( + "release_url", + "source_url", + "checksum_url", + "signature_url", + "certificate_url", + ): + _https(item.get(key), f"protocols.{name}.{key}") + for key in ("sha256", "checksum_sha256", "signature_sha256", "certificate_sha256"): + _digest(item.get(key), f"protocols.{name}.{key}", HEX_64) + + +def _validate_python(role: str, raw: Any, *, acceptance: bool = False) -> None: + item = _object(raw, f"artifacts.python.{role}") + if item.get("package") != "adcp": + raise InputError(f"artifacts.python.{role}.package must be 'adcp'") + version = _exact_version(item.get("version"), f"artifacts.python.{role}.version") + registry = _https(item.get("registry"), f"artifacts.python.{role}.registry") + if not registry.endswith("/"): + raise InputError(f"artifacts.python.{role}.registry must end with '/'") + url = _https(item.get("wheel_url"), f"artifacts.python.{role}.wheel_url") + _digest(item.get("wheel_sha256"), f"artifacts.python.{role}.wheel_sha256", HEX_64) + if not url.endswith(".whl") or version.replace("-", "_") not in url.replace("-", "_"): + raise InputError(f"artifacts.python.{role} wheel URL does not bind version {version}") + if item.get("source_kind") in {"checkout", "vcs", "pr", "source_tree"}: + raise InputError(f"artifacts.python.{role} is a source-only input") + protocol = _text(item.get("protocol"), f"artifacts.python.{role}.protocol") + expected_protocol = "3.2.0-rc.3" if role == "stable" else "3.2.0-rc.4" + if protocol != expected_protocol: + raise InputError( + f"artifacts.python.{role}.protocol must be {expected_protocol}, got {protocol}" + ) + if acceptance: + _exact_version(item.get("python_version"), f"artifacts.python.{role}.python_version") + _https( + item.get("requirements_lock_url"), + f"artifacts.python.{role}.requirements_lock_url", + ) + _digest( + item.get("requirements_lock_sha256"), + f"artifacts.python.{role}.requirements_lock_sha256", + HEX_64, + ) + + +def _validate_typescript(role: str, raw: Any, *, acceptance: bool = False) -> None: + item = _object(raw, f"artifacts.typescript.{role}") + if item.get("package") != "@adcp/sdk": + raise InputError(f"artifacts.typescript.{role}.package must be '@adcp/sdk'") + version = _exact_version(item.get("version"), f"artifacts.typescript.{role}.version") + if role == "candidate" and version == "14.0.0-rc.41": + raise InputError("14.0.0-rc.41 cannot substitute for the corrected TypeScript candidate") + if item.get("registry") != NPM_REGISTRY: + raise InputError(f"artifacts.typescript.{role}.registry must be {NPM_REGISTRY}") + url = _https(item.get("tarball_url"), f"artifacts.typescript.{role}.tarball_url") + expected_url = f"https://registry.npmjs.org/@adcp/sdk/-/sdk-{version}.tgz" + if url != expected_url: + raise InputError(f"artifacts.typescript.{role}.tarball_url must be {expected_url}") + integrity = _text(item.get("integrity"), f"artifacts.typescript.{role}.integrity") + if not integrity.startswith("sha512-"): + raise InputError(f"artifacts.typescript.{role}.integrity must be sha512") + try: + decoded = base64.b64decode(integrity.removeprefix("sha512-"), validate=True) + except ValueError as error: + raise InputError(f"artifacts.typescript.{role}.integrity is not valid base64") from error + if len(decoded) != 64: + raise InputError(f"artifacts.typescript.{role}.integrity is not a SHA-512 digest") + _digest(item.get("shasum"), f"artifacts.typescript.{role}.shasum", HEX_40) + if acceptance: + _exact_version(item.get("node_version"), f"artifacts.typescript.{role}.node_version") + _exact_version(item.get("npm_version"), f"artifacts.typescript.{role}.npm_version") + _https(item.get("lock_url"), f"artifacts.typescript.{role}.lock_url") + _digest( + item.get("lock_sha256"), + f"artifacts.typescript.{role}.lock_sha256", + HEX_64, + ) + + +def _validate_execution(manifest: Mapping[str, Any], *, acceptance: bool) -> None: + execution = _object(manifest.get("execution"), "execution") + if execution.get("database_encoding") != "UTF8" or execution.get("database_collation") != "C": + raise InputError("execution database identity must remain UTF8/C") + fixtures = _object(execution.get("fixture_identity"), "execution.fixture_identity") + expected_fixture_keys = { + "account_id", + "consumer_id", + "media_buy_id", + "delivery_config_id", + "reporting_obligation_id", + "reporting_revision_id", + "reporting_materialization_id", + } + if set(fixtures) != expected_fixture_keys: + raise InputError("execution.fixture_identity must contain every deterministic identifier") + for key, value in fixtures.items(): + _text(value, f"execution.fixture_identity.{key}") + if not acceptance: + return + postgres_version = _text(execution.get("postgresql_version"), "execution.postgresql_version") + if not re.fullmatch(r"[0-9]+\.[0-9]+(?:\.[0-9]+)?", postgres_version): + raise InputError("execution.postgresql_version must be exact") + _digest(execution.get("os_release_sha256"), "execution.os_release_sha256", HEX_64) + for name in ("seller", "buyer"): + path = _text(execution.get(f"{name}_entrypoint"), f"execution.{name}_entrypoint") + relative = Path(path) + if relative.is_absolute() or ".." in relative.parts: + raise InputError(f"execution.{name}_entrypoint must be a repository-relative path") + expected = _digest( + execution.get(f"{name}_entrypoint_sha256"), + f"execution.{name}_entrypoint_sha256", + HEX_64, + ) + resolved = (REPOSITORY_ROOT / relative).resolve() + if REPOSITORY_ROOT not in resolved.parents or not resolved.is_file(): + raise InputError(f"execution.{name}_entrypoint does not exist: {path}") + actual = _sha(resolved.read_bytes(), "sha256") + if actual != expected: + raise InputError(f"execution.{name}_entrypoint_sha256 does not match {path}") + + +def validate_manifest(manifest: Mapping[str, Any], *, acceptance: bool) -> Validation: + if manifest.get("schema_version") != 1: + raise InputError("schema_version must be 1") + protocols = _object(manifest.get("protocols"), "protocols") + if set(protocols) != {"stable", "candidate"}: + raise InputError("protocols must contain exactly stable and candidate") + for name, value in protocols.items(): + _validate_protocol(name, value) + _validate_execution(manifest, acceptance=False) + + artifacts = _object(manifest.get("artifacts"), "artifacts") + missing: list[str] = [] + for language, validator in (("python", _validate_python), ("typescript", _validate_typescript)): + roles = _object(artifacts.get(language), f"artifacts.{language}") + if set(roles) != {"stable", "candidate"}: + raise InputError(f"artifacts.{language} must contain exactly stable and candidate") + for role in ("stable", "candidate"): + value = roles[role] + if value is None: + missing.append(f"{language}.{role}") + else: + validator(role, value, acceptance=False) + + typescript_candidate = _object(artifacts["typescript"], "artifacts.typescript").get("candidate") + if typescript_candidate is not None: + pending_candidate = _object( + _object(manifest.get("pending"), "pending").get("typescript_candidate"), + "pending.typescript_candidate", + ) + proposed = _exact_version( + pending_candidate.get("proposed_version"), + "pending.typescript_candidate.proposed_version", + ) + observed = _exact_version( + _object(typescript_candidate, "artifacts.typescript.candidate").get("version"), + "artifacts.typescript.candidate.version", + ) + if observed != proposed: + raise InputError( + "artifacts.typescript.candidate must match the exact release proposed by #2979" + ) + + raw_cells = manifest.get("cells") + if not isinstance(raw_cells, list) or len(raw_cells) != 4: + raise InputError("cells must contain exactly four entries") + actual: list[tuple[str, str, str, str]] = [] + for index, raw in enumerate(raw_cells): + cell = _object(raw, f"cells[{index}]") + if cell.get("required") is not True: + raise InputError(f"cells[{index}] must remain required") + actual.append( + ( + _text(cell.get("id"), f"cells[{index}].id"), + _text(cell.get("python"), f"cells[{index}].python"), + _text(cell.get("typescript"), f"cells[{index}].typescript"), + _text(cell.get("protocol"), f"cells[{index}].protocol"), + ) + ) + if tuple(actual) != EXPECTED_CELLS: + raise InputError( + "cells must be the ordered stable/candidate cross-product with rc.3 skew " + "and rc.4 candidate/candidate" + ) + + if acceptance and missing: + raise InputError("acceptance inputs are incomplete: " + ", ".join(missing)) + if acceptance: + _validate_execution(manifest, acceptance=True) + for language, validator in ( + ("python", _validate_python), + ("typescript", _validate_typescript), + ): + roles = _object(artifacts.get(language), f"artifacts.{language}") + for role in ("stable", "candidate"): + validator(role, roles[role], acceptance=True) + if acceptance and manifest.get("phase") != "acceptance": + raise InputError("phase must be 'acceptance' before an acceptance run") + return Validation(ready=not missing, missing=tuple(missing)) + + +def _download(url: str) -> bytes: + request = urllib.request.Request(url, headers={"User-Agent": "adcp-reporting-interop/1"}) + with urllib.request.urlopen(request, timeout=120) as response: + return response.read() + + +def _sha(data: bytes, algorithm: str) -> str: + return hashlib.new(algorithm, data).hexdigest() + + +def _verify_python(item: Mapping[str, Any], output: Path) -> dict[str, Any]: + metadata_url = f"{str(item['registry']).rstrip('/')}/pypi/adcp/{item['version']}/json" + try: + metadata = json.loads(_download(metadata_url)) + except urllib.error.HTTPError as error: + raise InputError( + f"Python registry does not publish adcp=={item['version']}: HTTP {error.code}" + ) from error + files = metadata.get("urls") + if not isinstance(files, list): + raise InputError("Python registry metadata has no distribution list") + distributions = [ + entry + for entry in files + if isinstance(entry, dict) and entry.get("url") == item["wheel_url"] + ] + if len(distributions) != 1: + raise InputError("Python wheel URL is not uniquely present in exact registry metadata") + distribution = distributions[0] + digests = _object(distribution.get("digests"), "Python registry distribution digests") + if ( + distribution.get("packagetype") != "bdist_wheel" + or digests.get("sha256") != item["wheel_sha256"] + ): + raise InputError("Python registry wheel metadata does not match the manifest") + data = _download(str(item["wheel_url"])) + actual = _sha(data, "sha256") + if actual != item["wheel_sha256"]: + raise InputError( + f"Python wheel SHA-256 mismatch: expected {item['wheel_sha256']}, got {actual}" + ) + path = output / Path(str(item["wheel_url"])).name + path.write_bytes(data) + return { + "path": str(path), + "bytes": len(data), + "sha256": actual, + "url": item["wheel_url"], + "registry_metadata": { + "url": metadata_url, + "filename": distribution.get("filename"), + "packagetype": distribution.get("packagetype"), + "sha256": digests.get("sha256"), + }, + } + + +def _verify_typescript(item: Mapping[str, Any], output: Path) -> dict[str, Any]: + metadata_url = f"{NPM_REGISTRY}@adcp%2fsdk/{item['version']}" + try: + metadata = json.loads(_download(metadata_url)) + except urllib.error.HTTPError as error: + raise InputError( + f"npm does not publish @adcp/sdk@{item['version']}: HTTP {error.code}" + ) from error + dist = _object(metadata.get("dist"), "npm metadata dist") + observed = { + "version": metadata.get("version"), + "tarball_url": dist.get("tarball"), + "integrity": dist.get("integrity"), + "shasum": dist.get("shasum"), + } + expected = { + "version": item["version"], + "tarball_url": item["tarball_url"], + "integrity": item["integrity"], + "shasum": item["shasum"], + } + if observed != expected: + raise InputError(f"npm metadata mismatch: expected {expected}, got {observed}") + data = _download(str(item["tarball_url"])) + sha512 = base64.b64encode(hashlib.sha512(data).digest()).decode("ascii") + integrity = f"sha512-{sha512}" + shasum = _sha(data, "sha1") + if integrity != item["integrity"] or shasum != item["shasum"]: + raise InputError("downloaded npm tarball does not match registry integrity and shasum") + path = output / f"adcp-sdk-{item['version']}.tgz" + path.write_bytes(data) + return { + "path": str(path), + "bytes": len(data), + "integrity": integrity, + "shasum": shasum, + "url": item["tarball_url"], + "registry_metadata": observed, + } + + +def _verify_dependency_lock( + item: Mapping[str, Any], + *, + language: str, + role: str, + output: Path, +) -> dict[str, Any] | None: + url_key = "requirements_lock_url" if language == "python" else "lock_url" + digest_key = "requirements_lock_sha256" if language == "python" else "lock_sha256" + if item.get(url_key) is None and item.get(digest_key) is None: + return None + url = _https(item.get(url_key), f"artifacts.{language}.{role}.{url_key}") + expected = _digest(item.get(digest_key), f"artifacts.{language}.{role}.{digest_key}", HEX_64) + data = _download(url) + actual = _sha(data, "sha256") + if actual != expected: + raise InputError( + f"{language}.{role} dependency lock SHA-256 mismatch: " + f"expected {expected}, got {actual}" + ) + path = output / f"{language}-{role}-{Path(url).name}" + path.write_bytes(data) + return {"path": str(path), "url": url, "bytes": len(data), "sha256": actual} + + +def _verify_protocol(role: str, item: Mapping[str, Any], output: Path) -> dict[str, Any]: + release_api_url = ( + "https://api.github.com/repos/adcontextprotocol/adcp/releases/tags/" f"v{item['version']}" + ) + release = _object(json.loads(_download(release_api_url)), f"protocol {role} release") + if ( + release.get("tag_name") != f"v{item['version']}" + or release.get("target_commitish") != item["target_commit"] + or release.get("html_url") != item["release_url"] + ): + raise InputError(f"protocol {role} release identity does not match the manifest") + release_assets = release.get("assets") + if not isinstance(release_assets, list): + raise InputError(f"protocol {role} release assets are missing") + observed_assets = { + asset.get("browser_download_url"): asset.get("digest") + for asset in release_assets + if isinstance(asset, dict) + } + for url_key, digest_key in ( + ("source_url", "sha256"), + ("checksum_url", "checksum_sha256"), + ("signature_url", "signature_sha256"), + ("certificate_url", "certificate_sha256"), + ): + if observed_assets.get(item[url_key]) != f"sha256:{item[digest_key]}": + raise InputError(f"protocol {role} release metadata differs for {url_key}") + release_path = output / f"protocol-{role}-release.json" + release_path.write_text(json.dumps(release, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + assets: dict[str, Any] = {} + paths: dict[str, Path] = {} + for name, url_key, digest_key in ( + ("source", "source_url", "sha256"), + ("checksum", "checksum_url", "checksum_sha256"), + ("signature", "signature_url", "signature_sha256"), + ("certificate", "certificate_url", "certificate_sha256"), + ): + data = _download(str(item[url_key])) + actual = _sha(data, "sha256") + if actual != item[digest_key]: + raise InputError( + f"protocol {role} {name} SHA-256 mismatch: " + f"expected {item[digest_key]}, got {actual}" + ) + path = output / Path(str(item[url_key])).name + path.write_bytes(data) + paths[name] = path + assets[name] = { + "path": str(path), + "url": item[url_key], + "bytes": len(data), + "sha256": actual, + } + + checksum_text = paths["checksum"].read_text(encoding="ascii").strip().split()[0] + if checksum_text != item["sha256"]: + raise InputError(f"protocol {role} checksum sidecar does not name the source digest") + cosign = shutil.which("cosign") + if not cosign: + raise InputError("cosign is required to verify signed protocol inputs") + cosign_version = subprocess.run( + [cosign, "version", "--json"], check=True, text=True, capture_output=True + ).stdout.strip() + argv = [ + cosign, + "verify-blob", + "--signature", + str(paths["signature"]), + "--certificate", + str(paths["certificate"]), + "--certificate-identity", + "https://github.com/adcontextprotocol/adcp/.github/workflows/release.yml@refs/heads/main", + "--certificate-oidc-issuer", + "https://token.actions.githubusercontent.com", + str(paths["source"]), + ] + completed = subprocess.run(argv, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + if completed.returncode != 0: + raise InputError(f"cosign rejected protocol {role}: {completed.stdout.strip()}") + cosign_output = output / f"protocol-{role}-cosign.log" + cosign_output.write_text(completed.stdout, encoding="utf-8") + return { + "version": item["version"], + "target_commit": item["target_commit"], + "release_url": item["release_url"], + "release_metadata": _retained(release_path), + "assets": assets, + "cosign": { + "argv": argv, + "version": json.loads(cosign_version), + "exit_code": completed.returncode, + "output_path": str(cosign_output), + "output_sha256": _sha(completed.stdout.encode("utf-8"), "sha256"), + }, + } + + +def _run_logged(argv: list[str], *, cwd: Path, log: Path) -> subprocess.CompletedProcess[str]: + completed = subprocess.run( + argv, + cwd=cwd, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + log.write_text(completed.stdout, encoding="utf-8") + if completed.returncode != 0: + raise InputError( + f"installed baseline command failed ({completed.returncode}): {' '.join(argv)}; " + f"see {log}" + ) + return completed + + +def _retained(path: Path) -> dict[str, Any]: + data = path.read_bytes() + return {"path": str(path), "bytes": len(data), "sha256": _sha(data, "sha256")} + + +def _installed_baseline( + *, + output: Path, + python_item: Mapping[str, Any], + python_wheel: Path, + typescript_item: Mapping[str, Any], + typescript_tarball: Path, +) -> dict[str, Any]: + """Install current registry inputs in isolation as non-acceptance diagnostics.""" + output = output.resolve() + python_wheel = python_wheel.resolve() + typescript_tarball = typescript_tarball.resolve() + install_root = output / "installed-baseline" + install_root.mkdir() + python_root = install_root / "python" + node_root = install_root / "node" + python_root.mkdir() + node_root.mkdir() + python_executable = shutil.which("python3.12") + uv = shutil.which("uv") + npm = shutil.which("npm") + node = shutil.which("node") + if not all((python_executable, uv, npm, node)): + raise InputError("installed baseline requires python3.12, uv, node, and npm") + + venv = python_root / "venv" + _run_logged( + [str(uv), "venv", "--python", str(python_executable), str(venv)], + cwd=install_root, + log=install_root / "python-venv.log", + ) + cell_python = venv / "bin" / "python" + _run_logged( + [str(uv), "pip", "install", "--python", str(cell_python), str(python_wheel)], + cwd=install_root, + log=install_root / "python-install.log", + ) + python_probe = _run_logged( + [ + str(cell_python), + "-I", + "-c", + ( + "import adcp,json; from importlib.metadata import version; " + "from pathlib import Path; " + "print(json.dumps({'package_version':version('adcp'),'protocol_version':" + "Path(adcp.__file__).with_name('ADCP_VERSION').read_text().strip()," + "'module_origin':str(Path(adcp.__file__).resolve())},sort_keys=True))" + ), + ], + cwd=install_root, + log=install_root / "python-probe.json", + ) + python_probe_value = json.loads(python_probe.stdout) + if ( + python_probe_value.get("package_version") != python_item["version"] + or python_probe_value.get("protocol_version") != python_item["protocol"] + or str(venv.resolve()) not in python_probe_value.get("module_origin", "") + ): + raise InputError(f"installed Python baseline identity mismatch: {python_probe_value}") + freeze = _run_logged( + [str(uv), "pip", "freeze", "--python", str(cell_python)], + cwd=install_root, + log=install_root / "python-freeze.txt", + ) + + _run_logged([str(npm), "init", "-y"], cwd=node_root, log=install_root / "npm-init.log") + _run_logged( + [ + str(npm), + "install", + "--ignore-scripts", + "--no-audit", + "--no-fund", + "--save-exact", + str(typescript_tarball), + ], + cwd=node_root, + log=install_root / "npm-install.log", + ) + node_probe = _run_logged( + [ + str(node), + "--input-type=module", + "-e", + ( + "import {createRequire} from 'node:module'; " + "import {reconcileReporting} from '@adcp/sdk'; " + "const require=createRequire(import.meta.url); " + "const pkg=require('@adcp/sdk/package.json'); " + "console.log(JSON.stringify({package_version:pkg.version," + "reconcile_reporting_type:typeof reconcileReporting," + "module_origin:require.resolve('@adcp/sdk/package.json')}));" + ), + ], + cwd=node_root, + log=install_root / "node-probe.json", + ) + node_probe_value = json.loads(node_probe.stdout) + if ( + node_probe_value.get("package_version") != typescript_item["version"] + or node_probe_value.get("reconcile_reporting_type") != "function" + or str(node_root.resolve()) not in node_probe_value.get("module_origin", "") + ): + raise InputError(f"installed TypeScript baseline identity mismatch: {node_probe_value}") + npm_tree = _run_logged( + [str(npm), "ls", "--all", "--json"], + cwd=node_root, + log=install_root / "npm-tree.json", + ) + + retained_paths = [ + install_root / "python-venv.log", + install_root / "python-install.log", + install_root / "python-probe.json", + install_root / "python-freeze.txt", + install_root / "npm-init.log", + install_root / "npm-install.log", + install_root / "node-probe.json", + install_root / "npm-tree.json", + node_root / "package.json", + node_root / "package-lock.json", + ] + return { + "acceptance": False, + "reason": "surface/install diagnostic only; no PostgreSQL reporting service or matrix cell", + "python": python_probe_value, + "typescript": node_probe_value, + "runtime": { + "python": python_probe.args[0], + "node": subprocess.run( + [str(node), "--version"], check=True, text=True, capture_output=True + ).stdout.strip(), + "npm": subprocess.run( + [str(npm), "--version"], check=True, text=True, capture_output=True + ).stdout.strip(), + "os_release_sha256": _sha(Path("/etc/os-release").read_bytes(), "sha256"), + }, + "dependency_counts": { + "python_lines": len(freeze.stdout.splitlines()), + "npm_tree_bytes": len(npm_tree.stdout.encode("utf-8")), + }, + "evidence": [_retained(path) for path in retained_paths], + } + + +def verify_available( + manifest: Mapping[str, Any], + output: Path, + *, + include_baseline: bool, + installed_baseline: bool, +) -> dict[str, Any]: + validation = validate_manifest(manifest, acceptance=False) + output.mkdir(parents=True, exist_ok=False) + evidence: dict[str, Any] = { + "acceptance": False, + "created_at": datetime.now(timezone.utc).isoformat(), + "missing_acceptance_inputs": list(validation.missing), + "artifacts": {}, + "protocols": {}, + } + for role, item in _object(manifest["protocols"], "protocols").items(): + evidence["protocols"][role] = _verify_protocol( + role, _object(item, f"protocols.{role}"), output + ) + artifacts = _object(manifest["artifacts"], "artifacts") + verified_paths: dict[str, Path] = {} + for language in ("python", "typescript"): + for role in ("stable", "candidate"): + item = _object(artifacts[language], f"artifacts.{language}").get(role) + if item is None: + continue + key = f"{language}.{role}" + evidence["artifacts"][key] = ( + _verify_python(item, output) + if language == "python" + else _verify_typescript(item, output) + ) + dependency_lock = _verify_dependency_lock( + item, language=language, role=role, output=output + ) + if dependency_lock is not None: + evidence["artifacts"][key]["dependency_lock"] = dependency_lock + verified_paths[key] = Path(evidence["artifacts"][key]["path"]) + if include_baseline: + baseline = _object( + _object(manifest.get("baseline_inputs"), "baseline_inputs").get("python_registry"), + "baseline_inputs.python_registry", + ) + if baseline.get("acceptance") is not False: + raise InputError("baseline Python input must be explicitly non-acceptance") + evidence["artifacts"]["python.registry-baseline"] = _verify_python(baseline, output) + verified_paths["python.registry-baseline"] = Path( + evidence["artifacts"]["python.registry-baseline"]["path"] + ) + if installed_baseline: + if not include_baseline: + raise InputError("--installed-baseline requires --include-baseline") + typescript_stable = _object( + _object(artifacts.get("typescript"), "artifacts.typescript").get("stable"), + "artifacts.typescript.stable", + ) + evidence["installed_baseline"] = _installed_baseline( + output=output, + python_item=baseline, + python_wheel=verified_paths["python.registry-baseline"], + typescript_item=typescript_stable, + typescript_tarball=verified_paths["typescript.stable"], + ) + evidence_path = output / "evidence.json" + evidence_path.write_text( + json.dumps(evidence, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + return evidence + + +def _run_json(argv: Iterable[str]) -> Any: + try: + completed = subprocess.run(list(argv), check=True, text=True, capture_output=True) + except (OSError, subprocess.CalledProcessError) as error: + detail = ( + error.stderr.strip() + if isinstance(error, subprocess.CalledProcessError) and error.stderr + else str(error) + ) + raise InputError(f"command failed: {detail}") from error + try: + return json.loads(completed.stdout) + except json.JSONDecodeError as error: + raise InputError(f"command did not return JSON: {' '.join(argv)}") from error + + +def monitor_typescript(manifest: Mapping[str, Any]) -> dict[str, Any]: + pending = _object( + _object(manifest.get("pending"), "pending").get("typescript_candidate"), + "pending.typescript_candidate", + ) + repository = _text(pending.get("repository"), "pending.typescript_candidate.repository") + release_pr = pending.get("release_pr") + if not isinstance(release_pr, int): + raise InputError("pending.typescript_candidate.release_pr must be an integer") + expected_source = _digest( + pending.get("source_merge_commit"), + "pending.typescript_candidate.source_merge_commit", + HEX_40, + ) + proposed = _exact_version( + pending.get("proposed_version"), "pending.typescript_candidate.proposed_version" + ) + forbidden = _exact_version( + pending.get("forbidden_substitute"), "pending.typescript_candidate.forbidden_substitute" + ) + if proposed == forbidden: + raise InputError("proposed TypeScript candidate equals the forbidden prior release") + + pr = _run_json(("gh", "api", f"repos/{repository}/pulls/{release_pr}")) + body = str(pr.get("body") or "") + body_version = re.search(r"@adcp/sdk@([0-9A-Za-z.-]+)", body) + if not body_version or body_version.group(1) != proposed: + raise InputError(f"release PR does not propose exactly @adcp/sdk@{proposed}") + if ( + expected_source[:7] not in body + or _object(pr.get("base"), "release PR base").get("ref") != "main" + ): + raise InputError("release PR is not bound to the expected source change on main") + result: dict[str, Any] = { + "ready": False, + "acceptance": False, + "repository": repository, + "release_pr": release_pr, + "release_pr_url": pr.get("html_url"), + "release_pr_state": pr.get("state"), + "release_pr_merged": bool(pr.get("merged")), + "release_pr_merge_commit": pr.get("merge_commit_sha"), + "source_merge_commit": expected_source, + "proposed_version": proposed, + } + if not pr.get("merged"): + result["reason"] = "release PR is not merged; registry lookup is intentionally not accepted" + return result + + merge_commit = _digest(pr.get("merge_commit_sha"), "release PR merge_commit_sha", HEX_40) + runs = _run_json( + ( + "gh", + "api", + f"repos/{repository}/actions/runs?head_sha={merge_commit}&event=push&per_page=100", + ) + ) + workflow_runs = _object(runs, "release workflow runs").get("workflow_runs") + if not isinstance(workflow_runs, list): + raise InputError("GitHub did not return release workflow runs") + successful_release_runs = [ + run + for run in workflow_runs + if isinstance(run, dict) + and "release" in str(run.get("name", "")).lower() + and run.get("status") == "completed" + and run.get("conclusion") == "success" + and run.get("head_sha") == merge_commit + ] + if not successful_release_runs: + result["reason"] = "release PR merged but no successful exact-merge release workflow exists" + return result + + try: + metadata = json.loads(_download(f"{NPM_REGISTRY}@adcp%2fsdk/{proposed}")) + except urllib.error.HTTPError as error: + if error.code == 404: + result["reason"] = "release PR merged but exact npm package is not published" + return result + raise + dist = _object(metadata.get("dist"), "npm metadata dist") + if metadata.get("version") != proposed: + raise InputError("npm returned a mismatched package version") + integrity = _text(dist.get("integrity"), "npm dist.integrity") + shasum = _digest(dist.get("shasum"), "npm dist.shasum", HEX_40) + tarball_url = _https(dist.get("tarball"), "npm dist.tarball") + package = { + "package": "@adcp/sdk", + "version": proposed, + "registry": NPM_REGISTRY, + "tarball_url": tarball_url, + "integrity": integrity, + "shasum": shasum, + } + _validate_typescript("candidate", package) + with tempfile.TemporaryDirectory(prefix="adcp-ts-candidate-") as temporary: + verified = _verify_typescript(package, Path(temporary)) + result.update( + { + "ready": True, + "reason": "exact merged release workflow succeeded and registry bytes verify", + "release_workflows": [ + { + "database_id": run.get("id"), + "name": run.get("name"), + "head_sha": run.get("head_sha"), + "conclusion": run.get("conclusion"), + "html_url": run.get("html_url"), + } + for run in successful_release_runs + ], + "candidate": package, + "verification": {key: value for key, value in verified.items() if key != "path"}, + } + ) + return result + + +def _expected_input( + manifest: Mapping[str, Any], cell: Mapping[str, Any], language: str +) -> Mapping[str, Any]: + artifacts = _object(manifest.get("artifacts"), "artifacts") + role = _text(cell.get(language), f"cell.{language}") + return _object( + _object(artifacts.get(language), f"artifacts.{language}").get(role), + f"artifacts.{language}.{role}", + ) + + +def _validate_evidence_files(raw: Any, *, run_root: Path, field: str) -> None: + if not isinstance(raw, list) or not raw: + raise InputError(f"{field} must retain at least one evidence file") + root = run_root.resolve() + for index, entry_raw in enumerate(raw): + entry = _object(entry_raw, f"{field}[{index}]") + relative = Path(_text(entry.get("path"), f"{field}[{index}].path")) + if relative.is_absolute(): + raise InputError(f"{field}[{index}].path must be relative to the run root") + path = (root / relative).resolve() + if root not in path.parents: + raise InputError(f"{field}[{index}].path escapes the run root") + if not path.is_file(): + raise InputError(f"retained evidence is missing: {relative}") + data = path.read_bytes() + if entry.get("bytes") != len(data): + raise InputError(f"retained evidence byte count changed: {relative}") + expected = _digest(entry.get("sha256"), f"{field}[{index}].sha256", HEX_64) + if _sha(data, "sha256") != expected: + raise InputError(f"retained evidence digest changed: {relative}") + + +def validate_results( + manifest: Mapping[str, Any], results: Mapping[str, Any], *, run_root: Path +) -> None: + """Validate the retained legacy result shape without granting current acceptance.""" + + validate_manifest(manifest, acceptance=True) + if results.get("acceptance") is not True or results.get("status") != "passed": + raise InputError("aggregate result is not a passed acceptance result") + raw_cells = results.get("cells") + if not isinstance(raw_cells, list) or len(raw_cells) != 4: + raise InputError("aggregate result must contain exactly four cells") + expected_cells = manifest["cells"] + process_ids: set[int] = set() + databases: set[str] = set() + state_directories: set[str] = set() + for index, (raw, expected_raw) in enumerate(zip(raw_cells, expected_cells, strict=True)): + cell = _object(raw, f"results.cells[{index}]") + expected = _object(expected_raw, f"cells[{index}]") + if cell.get("id") != expected.get("id") or cell.get("status") != "passed": + raise InputError(f"results.cells[{index}] is missing or not passed") + if cell.get("acceptance") is not True: + raise InputError(f"results.cells[{index}] is not acceptance evidence") + pid = cell.get("seller_pid") + if not isinstance(pid, int) or pid <= 1 or pid in process_ids: + raise InputError("every cell must record a distinct seller process") + process_ids.add(pid) + database = _text(cell.get("database"), f"results.cells[{index}].database") + state_directory = _text( + cell.get("state_directory"), f"results.cells[{index}].state_directory" + ) + if database in databases or state_directory in state_directories: + raise InputError("every cell must use an isolated database and state directory") + databases.add(database) + state_directories.add(state_directory) + + assertions = _object(cell.get("assertions"), f"results.cells[{index}].assertions") + if tuple(assertions) != REQUIRED_ASSERTIONS or not all( + value is True for value in assertions.values() + ): + raise InputError( + f"results.cells[{index}] does not pass every required semantic assertion" + ) + + observed_inputs = _object(cell.get("inputs"), f"results.cells[{index}].inputs") + python_expected = _expected_input(manifest, expected, "python") + typescript_expected = _expected_input(manifest, expected, "typescript") + protocol_expected = _object( + _object(manifest.get("protocols"), "protocols").get(expected["protocol"]), + f"protocols.{expected['protocol']}", + ) + expected_identity = { + "python": { + "version": python_expected["version"], + "wheel_sha256": python_expected["wheel_sha256"], + "requirements_lock_sha256": python_expected["requirements_lock_sha256"], + }, + "typescript": { + "version": typescript_expected["version"], + "integrity": typescript_expected["integrity"], + "lock_sha256": typescript_expected["lock_sha256"], + }, + "protocol": { + "version": protocol_expected["version"], + "sha256": protocol_expected["sha256"], + }, + } + if observed_inputs != expected_identity: + raise InputError( + f"results.cells[{index}] artifact identity does not match the manifest" + ) + observed_runtime = _object(cell.get("runtime"), f"results.cells[{index}].runtime") + execution = _object(manifest.get("execution"), "execution") + expected_runtime = { + "python": python_expected["python_version"], + "node": typescript_expected["node_version"], + "npm": typescript_expected["npm_version"], + "postgresql": execution["postgresql_version"], + "os_release_sha256": execution["os_release_sha256"], + "database_encoding": execution["database_encoding"], + "database_collation": execution["database_collation"], + } + if observed_runtime != expected_runtime: + raise InputError(f"results.cells[{index}] runtime identity does not match") + _validate_evidence_files( + cell.get("evidence"), run_root=run_root, field=f"results.cells[{index}].evidence" + ) + + _validate_evidence_files(results.get("evidence"), run_root=run_root, field="results.evidence") + + +def _write_json(value: Any) -> None: + print(json.dumps(value, indent=2, sort_keys=True)) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST) + subparsers = parser.add_subparsers(dest="command", required=True) + validate = subparsers.add_parser("validate") + validate.add_argument("--acceptance", action="store_true") + verify = subparsers.add_parser("verify-available") + verify.add_argument("--output", type=Path, required=True) + verify.add_argument("--include-baseline", action="store_true") + verify.add_argument("--installed-baseline", action="store_true") + subparsers.add_parser("monitor-typescript") + results = subparsers.add_parser("validate-results") + results.add_argument("--results", type=Path, required=True) + results.add_argument("--run-root", type=Path, required=True) + args = parser.parse_args(argv) + + try: + manifest = _load(args.manifest) + if args.command == "validate": + result = validate_manifest(manifest, acceptance=args.acceptance) + _write_json( + { + "acceptance": args.acceptance, + "ready": result.ready, + "missing": list(result.missing), + } + ) + return 0 + if args.command == "verify-available": + _write_json( + verify_available( + manifest, + args.output, + include_baseline=args.include_baseline, + installed_baseline=args.installed_baseline, + ) + ) + return 0 + if args.command == "monitor-typescript": + result = monitor_typescript(manifest) + _write_json(result) + return 0 if result["ready"] else 3 + raw_results = _load(args.results) + validate_results(manifest, raw_results, run_root=args.run_root) + _write_json( + { + "acceptance": False, + "blocking_acceptance": False, + "status": "legacy_result_shape_validated_nonaccepting", + "validated_results": str(args.results), + "limitation": ( + "obsolete four-cell topology and synthetic assertions cannot confer " + "current reporting matrix acceptance" + ), + } + ) + return 0 + except InputError as error: + print(f"ERROR: {error}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_reporting_interop_corpus.py b/tests/test_reporting_interop_corpus.py new file mode 100644 index 000000000..f3c1bd337 --- /dev/null +++ b/tests/test_reporting_interop_corpus.py @@ -0,0 +1,1269 @@ +from __future__ import annotations + +import hashlib +import importlib.util +import json +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +DATA = ROOT / "scripts" / "ci" / "reporting_interop" + + +def load(name: str) -> dict: + return json.loads((DATA / name).read_text(encoding="utf-8")) + + +def load_python_module(name: str, path: Path): + spec = importlib.util.spec_from_file_location(name, path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def resolve_disposition(applicability: dict, scenario: dict, cell: dict) -> str: + default = applicability["defaults"][cell["family"]] + override = next( + (row for row in applicability["overrides"] if row["scenario"] == scenario["id"]), + None, + ) + if override is None: + return default + if "all" in override: + return override["all"] + if "by_family" in override: + return override["by_family"][cell["family"]] + if "by_client_language" in override: + value = override["by_client_language"][cell["client"]["language"]] + return default if value == "default" else value + if "by_server_scope" in override: + scope = "python_reporting_foundation" if cell["server"]["language"] == "python" else "other" + value = override["by_server_scope"][scope] + return default if value == "default" else value + raise AssertionError(f"unrecognized applicability override: {override}") + + +def test_corpus_declares_exact_candidate_language_quadrants() -> None: + applicability = load("applicability.json") + quadrants = [cell for cell in applicability["cells"] if cell["family"] == "candidate_quadrant"] + + assert {(cell["client"]["language"], cell["server"]["language"]) for cell in quadrants} == { + ("python", "python"), + ("typescript", "python"), + ("python", "typescript"), + ("typescript", "typescript"), + } + assert len(quadrants) == 4 + assert all(cell["blocking"] is True for cell in quadrants) + assert {cell["contract_id"] for cell in quadrants} == {"Q1", "Q2", "Q3", "Q4"} + assert all( + cell["protocol_contract"] + == {"required": "3.2.0-rc.6", "alignment": "exact", "selected_artifacts": False} + for cell in quadrants + ) + + +def test_skew_and_latest_canaries_are_separate_and_fail_closed() -> None: + applicability = load("applicability.json") + skew = [cell for cell in applicability["cells"] if cell["family"] == "supported_skew"] + canaries = [cell for cell in applicability["cells"] if cell["family"] == "latest_canary"] + + assert {cell["id"] for cell in skew} == { + "previous_py_client__candidate_ts_server", + "candidate_py_client__previous_ts_server", + } + assert all(cell["blocking"] is True for cell in skew) + assert {cell["contract_id"] for cell in skew} == {"S1", "S2"} + assert all( + cell["positive_requirement"] + == {"status": "unmet_no_published_compatible_pair", "blocking": True} + for cell in skew + ) + assert all( + cell["negative_skew_control"] + == {"required": True, "credit": "refusal_only_never_positive_acceptance"} + for cell in skew + ) + assert {cell["client"]["language"] for cell in canaries} == { + "python", + "typescript", + } + assert all(cell["blocking"] is False for cell in canaries) + + corpus = load("corpus.json") + scenario = next( + item + for item in corpus["scenarios"] + if item["id"] == "foundation.skew.actionable_unsupported" + ) + assert all( + resolve_disposition(applicability, scenario, cell) == "actionable_unsupported" + for cell in skew + ) + + +def test_every_scenario_has_a_valid_disposition_for_every_cell() -> None: + corpus = load("corpus.json") + applicability = load("applicability.json") + scenario_ids = [scenario["id"] for scenario in corpus["scenarios"]] + override_ids = [row["scenario"] for row in applicability["overrides"]] + + assert len(scenario_ids) == len(set(scenario_ids)) + assert len(override_ids) == len(set(override_ids)) + assert set(override_ids) <= set(scenario_ids) + allowed = set(applicability["dispositions"]) + for scenario in corpus["scenarios"]: + for cell in applicability["cells"]: + assert resolve_disposition(applicability, scenario, cell) in allowed + + +def test_staged_foundation_areas_cannot_be_dropped() -> None: + corpus = load("corpus.json") + areas = {scenario["area"] for scenario in corpus["scenarios"]} + + assert { + "capabilities", + "cli_storyboards", + "status_history", + "configuration", + "receipts", + "security", + "semantic_reconciliation", + "auth", + "isolation", + "value_integrity", + "pagination", + "packaging", + "revision_content", + "server_foundation_notifications", + "server_foundation_runtime", + "official_precedence", + "version_skew", + } <= areas + scenario_ids = {scenario["id"] for scenario in corpus["scenarios"]} + assert { + "foundation.pagination.zero_rows", + "foundation.pagination.more_than_500_rows", + "foundation.status.exact_revision_read", + "foundation.delivery.typed_exact_revision_pagination", + "foundation.security.ctx_metadata_credentials", + "foundation.security.resource_location_credential_screen", + "foundation.values.transport_large_integer_binding", + "foundation.cli.reporting_storyboards", + "foundation.packaging.reject_version_only_candidate", + "foundation.webhook.signed_retry_dedup", + "foundation.webhook.protocol_rc4_vectors", + "foundation.webhook.cross_language_signer_verifier", + "foundation.activity.durable_account_feed", + "foundation.runtime.late_account_materialization_fairness", + "foundation.runtime.producer_finality_temporal_order", + } <= scenario_ids + + +def test_security_hardening_rows_remain_visible_and_nonblocking() -> None: + corpus = load("corpus.json") + applicability = load("applicability.json") + scenarios = { + row["id"]: row + for row in corpus["scenarios"] + if row["id"] + in { + "foundation.security.ctx_metadata_credentials", + "foundation.security.resource_location_credential_screen", + } + } + + assert len(scenarios) == 2 + assert all(row["stage"] == "nonblocking_hardening" for row in scenarios.values()) + location = scenarios["foundation.security.resource_location_credential_screen"] + assert location["advisory_only"] is True + assert location["does_not_override_secret_leak_failure"] is True + assert location["evidence_scope"] == "in_process_installed_seller_persistence_guards_only" + assert all( + resolve_disposition(applicability, scenario, cell) == "visible_nonblocking" + for scenario in scenarios.values() + for cell in applicability["cells"] + ) + + +def test_resource_location_advisory_preserves_exact_safe_inputs() -> None: + security = DATA / "security" + expected_hashes = { + "resource_location_sentinels.json": ( + "082db6b6f64f2998920f5946382c0228178a80869f0ce65cc8cb044d19d8f065" + ), + "resource_location_python.original.py.txt": ( + "cf53cbfd9c5e5ffde723706254509ab4dc5c9d680462bad522bdfd16cdcdf94c" + ), + "resource_location_typescript.cjs": ( + "36e6894e98886de6848bf92ea6c9fa4299f540cafa73a81d5c27811f068ef1ca" + ), + } + + for name, expected in expected_hashes.items(): + assert hashlib.sha256((security / name).read_bytes()).hexdigest() == expected + sentinels = json.loads((security / "resource_location_sentinels.json").read_text()) + assert len(sentinels) == 17 + assert all("RLXSENTINEL" not in row["id"] for row in sentinels) + assert {row["expect"] for row in sentinels} == {"accept", "reject", "either"} + + +def test_official_precedence_vectors_require_fixed_behavior_without_exemption() -> None: + corpus = load("corpus.json") + vectors = { + scenario["id"]: scenario + for scenario in corpus["scenarios"] + if scenario["area"] == "official_precedence" + } + + assert set(vectors) == { + "foundation.official_precedence.unlinked_both_artifacts", + "foundation.official_precedence.no_official_materialization_unlinked", + "foundation.official_precedence.no_official_materialization_linked", + } + assert len({row["historical_vector"] for row in vectors.values()}) == 3 + assert all(row["required_outcome"] == "corrected_behavior" for row in vectors.values()) + assert all(row["known_defect_exemption"] is False for row in vectors.values()) + assert all(row["normative_rc4_fixture_coverage"] is False for row in vectors.values()) + assert load("corpus.json")["rules"]["expected_defect_exemptions"] is False + + +def test_language_specific_reconciliation_does_not_invent_python_facade() -> None: + corpus = load("corpus.json") + applicability = load("applicability.json") + python_scenario = next( + row + for row in corpus["scenarios"] + if row["id"] == "foundation.reconciliation.python_standalone" + ) + + assert all("client.reporting" not in entry for entry in python_scenario["public_entrypoints"]) + for cell in applicability["cells"]: + disposition = resolve_disposition(applicability, python_scenario, cell) + if cell["client"]["language"] == "python": + assert disposition in {"blocking_required", "visible_nonblocking"} + else: + assert disposition == "not_applicable" + + +@pytest.mark.parametrize( + "scenario_id", + [ + "foundation.webhook.signed_retry_dedup", + "foundation.activity.durable_account_feed", + "foundation.runtime.late_account_materialization_fairness", + "foundation.runtime.producer_finality_temporal_order", + ], +) +def test_server_foundation_scenarios_are_python_server_specific( + scenario_id: str, +) -> None: + corpus = load("corpus.json") + applicability = load("applicability.json") + scenario = next(row for row in corpus["scenarios"] if row["id"] == scenario_id) + + assert scenario["server_scope"] == "python_reporting_foundation" + for cell in applicability["cells"]: + disposition = resolve_disposition(applicability, scenario, cell) + if cell["server"]["language"] == "python": + expected = ( + "visible_nonblocking" if cell["family"] == "latest_canary" else "blocking_required" + ) + assert disposition == expected + else: + assert disposition == "not_applicable" + + +def test_protocol_webhook_vectors_are_not_replaced_by_narrow_signing_controls() -> None: + corpus = load("corpus.json") + applicability = load("applicability.json") + scenario = next( + row for row in corpus["scenarios"] if row["id"] == "foundation.webhook.protocol_rc4_vectors" + ) + + assert scenario["protocol_owned_inputs"] is True + assert "29" in scenario["expectation"] + assert "13-vector" in scenario["expectation"] + for cell in applicability["cells"]: + expected = ( + "visible_nonblocking" if cell["family"] == "latest_canary" else "blocking_required" + ) + assert resolve_disposition(applicability, scenario, cell) == expected + + +def test_cross_language_webhook_signing_is_a_distinct_blocking_row() -> None: + corpus = load("corpus.json") + applicability = load("applicability.json") + scenario = next( + row + for row in corpus["scenarios"] + if row["id"] == "foundation.webhook.cross_language_signer_verifier" + ) + + assert "Python-to-TypeScript" in scenario["expectation"] + assert "Content-Digest" in scenario["expectation"] + assert scenario["signature_profile"] == "webhook_signature_unpadded_base64url" + assert scenario["protocol_source_commit"] == "94976657c8456e5ad6de55d9793a883542a4fc5f" + for cell in applicability["cells"]: + expected = ( + "visible_nonblocking" if cell["family"] == "latest_canary" else "blocking_required" + ) + assert resolve_disposition(applicability, scenario, cell) == expected + + +def test_protocol_webhook_vector_runners_preserve_corrected_originals() -> None: + signing = DATA / "signing" + expected = { + "webhook_vectors_python.original.py.txt": ( + "86d9467ee2ecba518ebc5f89b751281c57865711c8fada107f98c61d94e26f68" + ), + "webhook_vectors_typescript.cjs": ( + "ad0c3d495c6a7f1fff066693251130b4c3fa2adb8286bc54d74adb06a19832dc" + ), + "signer_roundtrip_python.original.py.txt": ( + "c053d2b2951a5508fd332b64d5f834007ea4b355872625063048df8d8e1732b2" + ), + "crossverify_typescript.cjs": ( + "fb1b8e09d4459e9b705b3784b7a0c8018b0b870fb47f437118eb61e6cc5c84c9" + ), + "sign_webhook_typescript.cjs": ( + "07700c1fef060afe44ba705fad8d6532e5f2ef5fc519e38edfd0c9506224baa9" + ), + } + + for name, digest in expected.items(): + assert hashlib.sha256((signing / name).read_bytes()).hexdigest() == digest + + +def test_1172_and_1182_rows_remain_explicitly_deferred_in_every_cell() -> None: + corpus = load("corpus.json") + applicability = load("applicability.json") + deferred = [scenario for scenario in corpus["scenarios"] if scenario["area"] == "deferred"] + + assert {scenario["id"] for scenario in deferred} == { + "deferred.1172.service_composition", + "deferred.1172.client_reporting_facade", + "deferred.1182.provisional_reread", + } + assert all( + resolve_disposition(applicability, scenario, cell) == "deferred_open" + for scenario in deferred + for cell in applicability["cells"] + ) + + +def test_execution_contract_requires_real_isolated_process_boundary() -> None: + contract = load("applicability.json")["execution_contract"] + + assert contract == { + "fresh_processes_per_cell": True, + "isolated_database_per_cell": True, + "real_mcp_http": True, + "postgresql": "required_for_durable_seller_scenarios", + "installed_packages_only": True, + "latest_canaries_nonblocking": True, + } + + +def test_exact_candidate_pins_are_immutable_and_keep_source_separate() -> None: + pins = load("pins.json") + python = pins["python"]["candidate"] + typescript = pins["typescript"]["historical_candidate_rc45"] + + assert python["source_commit"] == "25e0c7278a19493345975881d1578c76fc64dc1b" + assert python["source_tree"] == "bcc4ad30ea4ee43d7b82f39e12f60b5a3ac2aead" + assert python["source_parents"] == [ + "7af569985174235739be4007e693d10ccb604817", + "fe1a1cbd070bc94ec32685026ad39ec55058dc73", + ] + assert python["adoption_base"] == "1f953c40d761be71d11fde84c78359ff2074fe7c" + assert python["build_kind"] == "prepublication_exact_pr1208_head" + assert python["version_string_occupied"] is True + assert "local_path_plus_sha256" in python["artifact_policy"] + assert len(python["known_independent_builds"]) == 3 + harness_build = next( + row + for row in python["known_independent_builds"] + if row["builder"] == "reporting_interop_harness" + ) + assert harness_build["all_member_manifest_sha256"] == ( + "397b9b58ad4e72d68e1380a5ad86ff9a76852ca873c3466b78d7c9890035cfa5" + ) + assert harness_build["sdk_member_manifest_sha256"] == ( + "0ed3b8de391a04be9aee477c6db11aa82e0edca12b1651ab9935dc83ba73b333" + ) + assert "registry" not in python + controls = python["development_dependency_controls"] + assert {(row["pydantic"], row["mcp"]) for row in controls} == { + ("2.13.0", "2.0.0"), + ("2.13.5", "2.2.0"), + } + for row in controls: + lock = ROOT / row["lock"] + assert hashlib.sha256(lock.read_bytes()).hexdigest() == row["lock_sha256"] + + assert typescript["version"] == "14.0.0-rc.45" + assert typescript["registry_git_head"] == "94171fe20d632f027eb362604c715ed361fadb51" + assert typescript["source_tree"] == "8d3c29d9a1779f04520c33cdc9ed3e15e5296cdd" + assert typescript["tarball_sha256"] == ( + "a0952ed8edaaad958bdb8f474c4f5cb68333ee272e7a8f3419d12930e9c57e6b" + ) + assert typescript["tarball_bytes"] == 19_598_459 + assert typescript["tarball_members"] == 6_246 + assert typescript["integrity"].startswith("sha512-") + assert typescript["version"] not in {"latest", "rc", "14.0.0-rc.41"} + lock = ROOT / typescript["package_lock"] + assert hashlib.sha256(lock.read_bytes()).hexdigest() == typescript["package_lock_sha256"] + locked_sdk = json.loads(lock.read_text())["packages"]["node_modules/@adcp/sdk"] + assert locked_sdk["version"] == typescript["version"] + assert locked_sdk["integrity"] == typescript["integrity"] + + development = pins["typescript"]["development_candidate_rc47"] + assert development["version"] == "14.0.0-rc.47" + assert development["protocol"] == "3.2.0-rc.6" + assert development["package_engines_node"] == "^20.19.0 || >=22.12.0" + assert development["development_runtime_pin"] == "22.12.0" + assert development["development_runtime_status"] == "selected_not_executed_by_this_harness" + assert pins["pending_source_leads"]["typescript_calendar"]["source_lineage"] == { + "pull_request": 3018, + "reviewed_pr_head": None, + "reviewed_pr_head_status": "not_recorded_in_current_harness_evidence", + "squash_merge_commit_prefix": "f3c7accb", + "published_registry_git_head": "b0d2886f0f5b8668fc134568c4cd22a4ef89e2fa", + "identities_are_not_interchangeable": True, + } + assert development["executable_harness_input"] is False + assert development["package_lock"] is None + assert development["transitive_lock"] is None + assert development["installed_member_binding"] is None + member_evidence = development["retained_member_evidence"] + assert member_evidence == { + "universal_compliance_subset_members": 55, + "scope": "universal_compliance_subset_only_not_complete_package_binding", + "complete_installed_members_expected": 6261, + "complete_installed_member_binding_available": False, + "can_substitute_for_transitive_lock": False, + } + assert pins["protocol"]["required_contract"] == { + "version": "3.2.0-rc.6", + "bundle_selected": False, + "status": "required_alignment_not_a_selected_protocol_bundle", + } + assert pins["protocol"]["historical_fixture_rc4"]["version"] == "3.2.0-rc.4" + latest = pins["typescript"]["latest_canary"] + assert latest["observed_version"] == "13.1.1" + assert latest["observed_at"] == "2026-09-25T00:00:00Z" + assert latest["execution_policy"] == "resolve_and_record_exact_current_identity_at_execution" + + historical = pins["typescript"]["historical_candidate_rc42"] + assert historical["version"] == "14.0.0-rc.42" + assert historical["qualification"] == "historical_matrix12_red_not_current_candidate" + + +def test_previous_typescript_skew_pin_is_exact_and_locked() -> None: + previous = load("pins.json")["typescript"]["previous_compatible"] + + assert previous["version"] == "14.0.0-rc.41" + assert previous["version"] not in {"latest", "rc", "adcp-3.1"} + assert previous["protocol"] == "3.2.0-rc.4" + assert previous["tarball_sha256"] == ( + "c9bb1e22b63dfedf6bc1ae9c07d1a1312465489671e9841dd5f1425b817b0a90" + ) + lock = ROOT / previous["package_lock"] + assert hashlib.sha256(lock.read_bytes()).hexdigest() == previous["package_lock_sha256"] + locked_sdk = json.loads(lock.read_text())["packages"]["node_modules/@adcp/sdk"] + assert locked_sdk["version"] == previous["version"] + assert locked_sdk["integrity"] == previous["integrity"] + + +def test_runner_executes_skew_storyboards_and_receiver_contracts_fail_closed() -> None: + runner = (DATA / "run_foundation_matrix.py").read_text(encoding="utf-8") + orchestration = (DATA / "storyboard_orchestration.py").read_text(encoding="utf-8") + python_client = (DATA / "python_core_client.py").read_text(encoding="utf-8") + receiver_server = (DATA / "signing" / "webhook_receiver_server.py").read_text(encoding="utf-8") + storyboard_inventory = (DATA / "ts_storyboard_inventory.cjs").read_text(encoding="utf-8") + assert "server.CONTROLLER_SCENARIOS || {}" not in storyboard_inventory + assert "public CONTROLLER_SCENARIOS export is missing or malformed" in storyboard_inventory + assert ( + "public CONTROLLER_SCENARIOS export contains no declared scenarios" in storyboard_inventory + ) + assert "source_member_identity" in storyboard_inventory + operation_map = load("controller-operation-map.json") + + assert "--previous-python-runtime" in runner + assert "--previous-python-artifact" in runner + assert 'role not in {"candidate", "historical_candidate", "previous", "lead"}' in runner + assert "candidate_py_client__previous_ts_server" in runner + assert "previous_py_client__candidate_ts_server" in runner + assert "expect_unsupported=True" in runner + assert '"mutation_attempted": False' in python_client + assert "--allow-historical-red" not in runner + assert 'result.get("blocking_acceptance") is not blocking_acceptance' in runner + assert "_execution_accounting" in runner + assert "observed_protocol_does_not_match_required" in runner + assert "observed_artifact_identity_does_not_match_required" in runner + assert "negative_or_partial_skew_cannot_satisfy_positive_requirement" in runner + assert "executed_visible_canaries" in runner + assert "missing_visible_canaries" in runner + + assert '"storyboard",' in orchestration + assert "storyboard selection drifted" in orchestration + assert "storyboard inventory must contain the exact ordered denominator" in orchestration + assert '"storyboard_executions": storyboard_executions' in runner + assert "resolved_steps" in orchestration + assert "executed_step_ids" in orchestration + assert "unexecuted_step_ids" in orchestration + assert "requires_all_capabilities" in storyboard_inventory + assert "controller_scenarios" in storyboard_inventory + assert "storyboardSteps" in storyboard_inventory + assert "storyboard step identities drifted" in storyboard_inventory + assert "controller_public_surface" in storyboard_inventory + assert "_validate_python_artifact_inputs" in runner + assert "sdk_member_manifest_sha256" in runner + assert "STORYBOARD_ORCHESTRATION" in runner + assert '"--external-pg-admin-dsn"' in runner + assert 'result.get("blocking_acceptance") is not False' in runner + assert {item["scenario"] for item in operation_map["scenarios"]} == { + "reporting_core_lifecycle_probe", + "reliable_reporting_core_integrity_probe", + "reliable_reporting_managed_delivery_probe", + "reliable_reporting_reconciled_billing_probe", + } + assert any( + operation["operation"] == "advance_time" + and operation["implementation"] + == "controlled_clock_public_store_and_real_mcp_preflight_passed" + for scenario in operation_map["scenarios"] + for operation in scenario["operations"] + ) + assert any( + operation["operation"] == "probe_scheduler_dst" + and operation["implementation"] == "rc47_development_selected_execution_pending" + and any( + boundary.endswith("producer.js:940-979") for boundary in operation["source_boundary"] + ) + for scenario in operation_map["scenarios"] + for operation in scenario["operations"] + ) + managed_operations = { + operation["operation"]: operation + for scenario in operation_map["scenarios"] + if scenario["scenario"] == "reliable_reporting_managed_delivery_probe" + for operation in scenario["operations"] + } + assert set(managed_operations) == { + "prepare", + "suppress_readiness", + "advance_within_retention", + "revoke_access", + } + assert all("preflight_passed" in row["implementation"] for row in managed_operations.values()) + billing_operations = { + operation["operation"]: operation + for scenario in operation_map["scenarios"] + if scenario["scenario"] == "reliable_reporting_reconciled_billing_probe" + for operation in scenario["operations"] + } + assert set(billing_operations) == {"prepare", "publish_adjustment"} + assert all("preflight_passed" in row["implementation"] for row in billing_operations.values()) + + controller_probe = (DATA / "ts_reporting_controller_probe.cjs").read_text() + assert "handleTestControllerRequest" in controller_probe + assert "createReportingProducer" in controller_probe + assert "PostgresReportingLedgerStore" in controller_probe + assert "blocking_acceptance: false" in controller_probe + assert "literal_storyboard_identity_match" in controller_probe + + core_server = (DATA / "ts_core_server.cjs").read_text() + assert "createSyncReportingStatusHandler" in core_server + assert "consumer_status_task: 'sync_reporting_status'" in core_server + assert "mcp.registerTool('sync_reporting_status'" in core_server + assert "TOKEN_BINDINGS" not in core_server + assert "auth-token-a" not in core_server and "auth-token-b" not in core_server + assert "corePrivateInputs" in core_server + assert "process_start_token" in core_server + assert "startup_proof_sha256" in core_server + assert "seller_package" in core_server + assert "wire_adcp_version" in core_server + assert "flag: 'wx', mode: 0o600" in core_server + + controlled_store = (DATA / "ts_controlled_reporting_fixture.cjs").read_text() + controlled_server = (DATA / "ts_controlled_reporting_server.cjs").read_text() + controlled_probe = (DATA / "ts_controlled_reporting_probe.cjs").read_text() + controlled_http_probe = (DATA / "ts_controlled_reporting_http_probe.cjs").read_text() + assert "class ControlledReportingStore" in controlled_store + assert "syncConsumerStatusBatch" in controlled_store + assert "literal reporting vector digest changed" in controlled_store + assert "TOOL_INPUT_SHAPE" in controlled_server + assert "mcp.registerTool('comply_test_controller'" in controlled_server + assert "createReportingStatusHandler" in controlled_server + assert "createSyncReportingStatusHandler" in controlled_server + assert "controlled_clock_fixture_not_postgresql" in controlled_server + assert "probe_scheduler_dst_unavailable" in controlled_server + assert "reporting-revision.ecc62efa00946aa1e2788ad9" in controlled_probe + assert "idempotent replay must return the retained ordered result" in controlled_probe + assert "blocking_acceptance: false" in controlled_probe + assert "ProtocolClient.callTool" in controlled_http_probe + assert "reporting-revision.ecc62efa00946aa1e2788ad9" in controlled_http_probe + assert "sync_reporting_status" in controlled_http_probe + assert "focused_real_mcp_preflight_not_cli_storyboard_execution" in controlled_http_probe + + managed_fixture = (DATA / "ts_managed_reporting_fixture.cjs").read_text() + managed_server = (DATA / "ts_managed_reporting_server.cjs").read_text() + managed_probe = (DATA / "ts_managed_reporting_probe.cjs").read_text() + managed_http_probe = (DATA / "ts_managed_reporting_http_probe.cjs").read_text() + assert "PostgresReportingLedgerStore" in managed_fixture + assert "PostgresReportingManagedDeliveryStore" in managed_fixture + assert "createReportingManagedDeliveryRuntime" in managed_fixture + assert "authorizeDestination" in managed_fixture + assert managed_fixture.index("installBinding") < managed_fixture.index("putObligation") + assert "runtime.readResource" in managed_fixture + assert "revokeDestination" in managed_fixture + assert "runtime.runWorker" in managed_fixture + assert "reportingCanonicalAdjustmentSha256V1" in managed_fixture + assert "core.commitAdjustment" in managed_fixture + assert "syncReceiptBatch" not in managed_fixture + assert "mcp.registerTool('comply_test_controller'" in managed_server + assert "mcp.registerTool('sync_reporting_receipts'" in managed_server + assert "runtime.syncReportingReceipts" in managed_server + assert "focused_public_controller_preflight_not_storyboard_execution" in managed_probe + assert "stale_after_terminal" in managed_probe + assert "ProtocolClient.callTool" in managed_http_probe + assert "focused_real_mcp_preflight_not_cli_storyboard_execution" in managed_http_probe + assert "const account = { account_id: accountId, sandbox: true }" in managed_http_probe + assert "expectSelectorRejected({ account_id: accountId })" in managed_http_probe + assert "expectSelectorRejected({ account_id: accountId, sandbox: false })" in managed_http_probe + assert "selector_negatives: selectorNegatives" in managed_http_probe + assert "probe_scheduler_dst" not in managed_fixture + assert "blocking_acceptance: false" in managed_probe + assert "blocking_acceptance: false" in managed_http_probe + + abort_helper = (DATA / "ts_probe_abort.cjs").read_text() + assert "controller.abort(error)" in abort_helper + assert "process.once('SIGINT'" in abort_helper + assert "process.once('SIGTERM'" in abort_helper + for probe in [ + controlled_http_probe, + managed_http_probe, + (DATA / "ts_core_buyer.cjs").read_text(), + (DATA / "ts_mcp_buyer.cjs").read_text(), + ]: + assert "callWithProbeAbort" in probe + assert "signal," in probe or "signal, transport" in probe + + facade_probe = (DATA / "ts_primary_facade_gap.cjs").read_text() + assert "semantic_lane_complete: false" in facade_probe + assert "surface_available: !gapReproduced" in facade_probe + assert "surface_available_nonsemantic" in facade_probe + + assert "WebhookReceiver" in receiver_server + assert "CachingRevocationChecker" in receiver_server + assert "RevocationListFetcher" in receiver_server + + +def test_ts_version_and_abort_helpers_fail_closed_without_sdk_execution() -> None: + receiver_server = (DATA / "signing" / "webhook_receiver_server.py").read_text() + receiver_client = (DATA / "signing" / "webhook_receiver_client.py").read_text() + script = f""" + const assert = require('node:assert/strict'); + const nodeCrypto = require('node:crypto'); + const version = require({json.dumps(str(DATA / 'ts_adcp_version.cjs'))}); + const abort = require({json.dumps(str(DATA / 'ts_probe_abort.cjs'))}); + const controlled = require({json.dumps(str(DATA / 'ts_controlled_reporting_server.cjs'))}); + const managed = require({json.dumps(str(DATA / 'ts_managed_reporting_server.cjs'))}); + const core = require({json.dumps(str(DATA / 'ts_core_server.cjs'))}); + const privateInputsModule = require({json.dumps(str(DATA / 'ts_private_inputs.cjs'))}); + assert.equal(version.wireAdcpVersion('3.2.0-rc.6'), '3.2-rc.6'); + assert.equal(version.wireAdcpVersion('3.1.20'), '3.1'); + assert.equal(version.wireAdcpVersion('3.2.1'), '3.2'); + assert.equal(version.wireAdcpVersion('3.2.1-rc.6'), '3.2-rc.6'); + assert.throws(() => version.wireAdcpVersion('3.2')); + assert.throws(() => version.wireAdcpVersion('03.2.1')); + assert.throws(() => version.wireAdcpVersion('3.2.1-rc..6')); + assert.throws(() => version.wireAdcpVersion('3.2.1-rc.06')); + assert.equal(version.assertCapabilityVersion, undefined); + version.assertFixtureCapabilityVersion({{ + adcp_version: '3.2-rc.6', adcp: {{ supported_versions: ['3.2-rc.6'] }}, + }}, '3.2.0-rc.6'); + assert.throws(() => version.assertFixtureCapabilityVersion({{ + adcp_version: '3.2-rc.4', adcp: {{ supported_versions: ['3.2-rc.4'] }}, + }}, '3.2.0-rc.6')); + assert.throws(() => version.assertFixtureCapabilityVersion({{ + adcp_version: '3.2-rc.6', adcp: {{ supported_versions: ['3.2-rc.6', '3.1'] }}, + }}, '3.2.0-rc.6')); + let canonicalPayload; + const validator = value => {{ canonicalPayload = value; return true; }}; + validator.errors = []; + const api = {{ schemas: {{ + getCanonicalToolValidator(tool, variant, options) {{ + assert.equal(tool, 'get_adcp_capabilities'); + assert.equal(variant, 'sync'); + assert.deepEqual(options, {{ adcpVersion: '3.2.0-rc.6' }}); + return validator; + }}, + }} }}; + const advertised = controlled.validateAdvertisedCapabilities(api, '3.2.0-rc.6'); + assert.equal(advertised.adcp_version, '3.2-rc.6'); + assert.deepEqual(canonicalPayload, {{ ...advertised, status: 'completed' }}); + assert.throws(() => controlled.validateAdvertisedCapabilities( + api, + '3.2.0-rc.6', + controlled.capabilities('3.2.0-rc.4'), + ), /wire version does not match/); + (async () => {{ + const outputs = []; + const fakeApi = {{ + expectedIntegrity: 'sha512-fixture', + installed: {{ name: '@adcp/sdk', version: '14.0.0-test' }}, + schemas: api.schemas, + }}; + await controlled.main(['--probe', '--adcp-version', '3.2.0-rc.6'], {{ + loadPinned: () => fakeApi, + createControlledController: () => ({{ factory: {{ scenarios: ['core'] }} }}), + writeOutput: value => outputs.push(JSON.parse(value)), + }}); + class FakePool {{ + async query() {{ return {{}}; }} + async end() {{}} + }} + const managedApi = {{ + ...fakeApi, + pg: {{ Pool: FakePool }}, + }}; + await managed.main([ + '--probe', '--mode', 'managed', '--pg-url', 'postgresql://fixture/db', + '--adcp-version', '3.2.0-rc.6', + ], {{ + loadPinned: () => managedApi, + createManagedReportingFixture: async () => ({{ + mode: 'managed', runtime: {{ reportingDeliveryCapabilities: {{ supported: true }} }}, + }}), + writeOutput: value => outputs.push(JSON.parse(value)), + }}); + await core.run(['--probe', '--adcp-version', '3.2.0-rc.6'], {{ + packageContext: () => ({{ + expectedIntegrity: 'sha512-fixture', installed: {{ version: '14.0.0-test' }}, + packageName: '@adcp/sdk', + }}), + loadPublicSurface: () => fakeApi, + emit: (_stream, value) => outputs.push(value), + stdout: {{}}, + }}); + assert.deepEqual(outputs.map(value => value.kind), [ + 'controlled_reporting_storyboard_server_probe', + 'managed_reporting_storyboard_server_probe', + 'reporting_interop_ts_core_server_probe', + ]); + const rejectingApi = {{ schemas: {{ + getCanonicalToolValidator() {{ + const reject = () => false; reject.errors = [{{ message: 'wrong version' }}]; + return reject; + }}, + }} }}; + await assert.rejects(controlled.main( + ['--probe', '--adcp-version', '3.2.0-rc.6'], + {{ + loadPinned: () => rejectingApi, + createControlledController: () => ({{ factory: {{ scenarios: [] }} }}), + writeOutput: () => {{}}, + }}, + ), /wrong version/); + await assert.rejects(managed.main([ + '--probe', '--mode', 'managed', '--pg-url', 'postgresql://fixture/db', + '--adcp-version', '3.2.0-rc.6', + ], {{ + loadPinned: () => ({{ ...managedApi, schemas: rejectingApi.schemas }}), + createManagedReportingFixture: async () => ({{ + mode: 'managed', runtime: {{ reportingDeliveryCapabilities: {{ supported: true }} }}, + }}), + writeOutput: () => {{}}, + }}), /wrong version/); + await assert.rejects(core.run(['--probe', '--adcp-version', '3.2.0-rc.6'], {{ + packageContext: () => ({{ + expectedIntegrity: 'sha512-fixture', installed: {{ version: '14.0.0-test' }}, + packageName: '@adcp/sdk', + }}), + loadPublicSurface: () => rejectingApi, + emit: () => {{}}, + stdout: {{}}, + }}), /wrong version/); + let controlledFixtureCalls = 0; + await assert.rejects(controlled.main([ + '--adcp-version', '3.2.0-rc.6', '--port', '55555', + '--ready-file', '/owned/controlled.ready', + ], {{ + loadPinned: () => fakeApi, + environment: {{ + ADCP_INTEROP_TS_STORYBOARD_AUTH_TOKEN: 'short', + ADCP_INTEROP_TS_STORYBOARD_STARTUP_PROOF: 'b'.repeat(64), + }}, + createControlledController() {{ controlledFixtureCalls += 1; return {{}}; }}, + }}), /strong per-run private input/); + assert.equal(controlledFixtureCalls, 0); + await assert.rejects(core.run([ + '--adcp-version', '3.2.0-rc.6', '--port', '55555', + ], {{ + packageContext: () => ({{ + expectedIntegrity: 'sha512-fixture', installed: {{ version: '14.0.0-test' }}, + packageName: '@adcp/sdk', + }}), + loadPublicSurface: () => fakeApi, + environment: {{ + ADCP_INTEROP_TS_CORE_AUTH_TOKEN_A: 'short', + ADCP_INTEROP_TS_CORE_AUTH_TOKEN_B: 'b'.repeat(43), + ADCP_INTEROP_TS_CORE_STARTUP_PROOF: 'c'.repeat(64), + }}, + }}), /strong per-run private input/); + const strongA = nodeCrypto.randomBytes(32).toString('base64url'); + const strongB = nodeCrypto.randomBytes(32).toString('base64url'); + const privateEnvironment = {{ + ADCP_INTEROP_TS_CORE_AUTH_TOKEN_A: strongA, + ADCP_INTEROP_TS_CORE_AUTH_TOKEN_B: strongB, + ADCP_INTEROP_TS_CORE_STARTUP_PROOF: nodeCrypto.randomBytes(32).toString('hex'), + }}; + assert.throws(() => core.corePrivateInputs({{ + ...privateEnvironment, ADCP_INTEROP_TS_CORE_AUTH_TOKEN_A: 'fixed-a', + }}), /strong per-run private input/); + assert.throws(() => core.corePrivateInputs({{ + ...privateEnvironment, ADCP_INTEROP_TS_CORE_AUTH_TOKEN_B: strongA, + }}), /distinct per-run tokens/); + const privateInputs = core.corePrivateInputs(privateEnvironment); + const bindings = core.authenticationBindings(privateInputs); + assert.equal(bindings.get(strongA).account_id, 'interop-account-a'); + const storyboardEnvironment = {{ + ADCP_INTEROP_TS_STORYBOARD_AUTH_TOKEN: strongA, + ADCP_INTEROP_TS_STORYBOARD_STARTUP_PROOF: privateInputs.startupProof, + }}; + assert.deepEqual( + privateInputsModule.storyboardPrivateInputs(storyboardEnvironment), + {{ authToken: strongA, startupProof: privateInputs.startupProof }}, + ); + assert.throws(() => privateInputsModule.storyboardPrivateInputs({{ + ...storyboardEnvironment, ADCP_INTEROP_TS_STORYBOARD_STARTUP_PROOF: 'short', + }}), /strong per-run private input/); + const ready = core.buildReadyRecord({{ + expectedIntegrity: 'sha512-fixture', installed: {{ version: '14.0.0-test' }}, + packageName: '@adcp/sdk', + }}, {{ + adcpVersion: '3.2.0-rc.6', authBindings: bindings, port: 55555, + sellerRole: 'candidate', startupProof: privateInputs.startupProof, + url: 'http://127.0.0.1:55555/mcp', + }}); + const encodedReady = JSON.stringify(ready); + assert.equal(encodedReady.includes(strongA), false); + assert.equal(encodedReady.includes(strongB), false); + assert.equal(encodedReady.includes(privateInputs.startupProof), false); + assert.equal(ready.seller_package.package_role, 'candidate'); + assert.equal(ready.seller_package.protocol, '3.2.0-rc.6'); + for (const fixtureReady of [ + controlled.buildReadyRecord(fakeApi, {{ + adcpVersion: '3.2.0-rc.6', authToken: strongA, port: 55555, + startupProof: privateInputs.startupProof, + url: 'http://127.0.0.1:55555/mcp', + }}), + managed.buildReadyRecord(fakeApi, {{ + adcpVersion: '3.2.0-rc.6', authToken: strongA, mode: 'managed', port: 55555, + startupProof: privateInputs.startupProof, + url: 'http://127.0.0.1:55555/mcp', + }}), + ]) {{ + const encoded = JSON.stringify(fixtureReady); + assert.equal(encoded.includes(strongA), false); + assert.equal(encoded.includes(privateInputs.startupProof), false); + assert.equal(fixtureReady.startup_proof_sha256.length, 64); + }} + const boundary = abort.createProbeAbort('unit'); + await assert.rejects(abort.callWithProbeAbort(boundary, async () => {{ + throw new Error('first failure'); + }}), /first failure/); + assert.equal(boundary.signal.aborted, true); + boundary.dispose(); + }})().catch(error => {{ console.error(error); process.exitCode = 1; }}); + """ + subprocess.run(["node", "-e", script], check=True, cwd=ROOT) + assert "urllib.request.urlopen" in receiver_server + assert '"normative_019_match"' in receiver_client + assert '"blocking_acceptance"' in receiver_client + assert '"historical_reproducer_mode"' in receiver_client + assert '"replay_persistence_exercised": False' in receiver_client + assert '"stale_failure_classification"' in receiver_client + assert "RevocationListFreshnessError" in receiver_client + assert "stale_historical_red" in receiver_client + assert '"emitted_headers"' not in receiver_client + assert '"private_key"' not in receiver_client + + +def test_managed_server_owns_every_resource_phase_with_recording_fakes() -> None: + script = r""" + const assert = require('node:assert/strict'); + const managed = require(__MANAGED__); + + const strongEnvironment = { + ADCP_INTEROP_TS_STORYBOARD_AUTH_TOKEN: 'a'.repeat(43), + ADCP_INTEROP_TS_STORYBOARD_STARTUP_PROOF: 'b'.repeat(64), + }; + const normalArgs = [ + '--mode', 'managed', '--pg-url', 'postgresql://fixture/db', + '--adcp-version', '3.2.0-rc.6', '--port', '55555', + '--ready-file', '/owned/ready.json', + ]; + + function fixture(kind) { + const events = []; + let poolCount = 0; + class FakePool { + constructor() { + this.id = ++poolCount; + events.push(`new:${this.id}`); + if (kind === 'fixture-pool-constructor' && this.id === 2) { + throw new Error('fixture pool constructor failed'); + } + } + async query(statement) { + const verb = String(statement).split(' ')[0]; + events.push(`query:${this.id}:${verb}`); + if (kind === 'schema-create' && verb === 'CREATE') { + throw new Error('schema create failed'); + } + } + async end() { + events.push(`end:${this.id}`); + if (kind === 'cleanup-and-primary' && this.id === 2) { + throw new Error('fixture pool cleanup failed'); + } + } + } + const validator = value => { + events.push(`validate:${value.adcp_version}`); + return kind !== 'canonical'; + }; + validator.errors = [{ message: 'canonical rejection' }]; + const api = { + expectedIntegrity: 'sha512-fixture', + installed: { name: '@adcp/sdk', version: '14.0.0-test' }, + pg: { Pool: FakePool }, + schemas: { getCanonicalToolValidator() { return validator; } }, + server: { + verifyApiKey() { + events.push('verify-api-key'); + if (kind === 'authentication') throw new Error('authentication setup failed'); + return {}; + }, + serve(_factory, options) { + events.push('serve'); + if (kind === 'serve') throw new Error('serve construction failed'); + const listeners = new Map(); + const server = { + closed: false, + on(event, listener) { + events.push(`server:on:${event}`); + listeners.set(event, listener); + return this; + }, + removeListener(event, listener) { + events.push(`server:remove:${event}`); + if (listeners.get(event) === listener) listeners.delete(event); + return this; + }, + emit(event, value) { + const listener = listeners.get(event); + if (listener) { + listener(value); + return true; + } + if (event === 'error') throw value; + return false; + }, + close(callback) { + events.push('server:close'); + this.closed = true; + callback( + ['close-failure', 'bind-error-close-failure'].includes(kind) + ? new Error('server close failed') + : undefined, + ); + }, + }; + queueMicrotask(() => { + if (kind === 'bind-error-close-failure') { + server.emit('error', new Error('bind failed before listening')); + } else { + options.onListening('http://127.0.0.1:55555/mcp'); + } + }); + return server; + }, + }, + }; + const dependencies = { + loadPinned: () => api, + environment: strongEnvironment, + async createManagedReportingFixture() { + events.push('fixture'); + if (['fixture', 'cleanup-and-primary'].includes(kind)) { + throw new Error('fixture construction failed'); + } + return { + mode: 'managed', + runtime: { reportingDeliveryCapabilities: { supported: true } }, + }; + }, + registerShutdownHandlers() { + events.push('handlers:install'); + return () => events.push('handlers:remove'); + }, + writeReadyFile(_path, _value, options) { + events.push(`ready:${options.flag}:${options.mode.toString(8)}`); + if (kind === 'ready') throw new Error('ready file already exists'); + }, + writeOutput() { events.push('output'); }, + }; + return { api, dependencies, events, get poolCount() { return poolCount; } }; + } + + (async () => { + { + const state = fixture('missing-version'); + await assert.rejects(managed.main([ + '--mode', 'managed', '--pg-url', 'postgresql://fixture/db', '--probe', + ], state.dependencies), /missing --adcp-version/); + assert.equal(state.poolCount, 0); + } + { + const state = fixture('bad-private-input'); + await assert.rejects(managed.main(normalArgs, { + ...state.dependencies, + environment: { + ...strongEnvironment, + ADCP_INTEROP_TS_STORYBOARD_AUTH_TOKEN: 'short', + }, + }), /strong per-run private input/); + assert.equal(state.poolCount, 0); + } + for (const [kind, message] of [ + ['schema-create', 'schema create failed'], + ['fixture-pool-constructor', 'fixture pool constructor failed'], + ['fixture', 'fixture construction failed'], + ['canonical', 'canonical rejection'], + ['authentication', 'authentication setup failed'], + ['serve', 'serve construction failed'], + ['ready', 'ready file already exists'], + ]) { + const state = fixture(kind); + await assert.rejects(managed.main(normalArgs, state.dependencies), new RegExp(message)); + if (kind === 'schema-create') { + assert.deepEqual(state.events, ['new:1', 'query:1:CREATE', 'end:1']); + continue; + } + if (kind === 'fixture-pool-constructor') { + assert.equal(state.events.includes('end:2'), false); + } else { + assert.equal(state.events.includes('end:2'), true, `${kind}: fixture pool not ended`); + } + assert.equal(state.events.includes('query:1:DROP'), true, `${kind}: schema not dropped`); + assert.equal(state.events.includes('end:1'), true, `${kind}: bootstrap not ended`); + if (kind === 'ready') { + assert.ok(state.events.indexOf('server:close') < state.events.indexOf('end:2')); + } + } + { + const state = fixture('probe-success'); + await managed.main([ + '--probe', '--mode', 'managed', '--pg-url', 'postgresql://fixture/db', + '--adcp-version', '3.2.0-rc.6', + ], state.dependencies); + assert.equal(state.events.includes('serve'), false); + assert.equal(state.events.filter(value => value === 'end:2').length, 1); + assert.equal(state.events.filter(value => value === 'query:1:DROP').length, 1); + assert.equal(state.events.filter(value => value === 'end:1').length, 1); + } + { + const state = fixture('success'); + const running = await managed.main(normalArgs, state.dependencies); + assert.equal(state.events.includes('server:close'), false); + await running.close(); + assert.equal(state.events.filter(value => value === 'server:close').length, 1); + assert.equal(state.events.filter(value => value === 'end:2').length, 1); + assert.equal(state.events.filter(value => value === 'query:1:DROP').length, 1); + assert.equal(state.events.filter(value => value === 'end:1').length, 1); + await running.close(); + assert.equal(state.events.filter(value => value === 'server:close').length, 1); + } + { + const state = fixture('close-failure'); + const running = await managed.main(normalArgs, state.dependencies); + await assert.rejects(running.close(), /resource cleanup failed/); + await assert.rejects(running.close(), /resource cleanup failed/); + assert.equal(state.events.filter(value => value === 'server:close').length, 1); + assert.equal(state.events.filter(value => value === 'end:2').length, 1); + assert.equal(state.events.filter(value => value === 'query:1:DROP').length, 1); + assert.equal(state.events.filter(value => value === 'end:1').length, 1); + } + { + const state = fixture('bind-error-close-failure'); + await assert.rejects(managed.main(normalArgs, state.dependencies), error => { + assert.equal(error instanceof AggregateError, true); + assert.match(error.errors[0].message, /bind failed before listening/); + assert.match(error.errors[1].message, /resource cleanup failed/); + return true; + }); + assert.equal(state.events.includes('ready:wx:600'), false); + assert.equal(state.events.includes('output'), false); + assert.equal(state.events.filter(value => value === 'server:on:error').length, 1); + assert.equal(state.events.filter(value => value === 'server:close').length, 1); + assert.equal(state.events.filter(value => value === 'server:remove:error').length, 1); + assert.equal(state.events.filter(value => value === 'end:2').length, 1); + assert.equal(state.events.filter(value => value === 'query:1:DROP').length, 1); + assert.equal(state.events.filter(value => value === 'end:1').length, 1); + assert.ok(state.events.indexOf('server:close') < state.events.indexOf('end:2')); + assert.ok(state.events.indexOf('server:remove:error') < state.events.indexOf('end:2')); + } + { + const state = fixture('cleanup-and-primary'); + await assert.rejects(managed.main(normalArgs, state.dependencies), error => { + assert.equal(error instanceof AggregateError, true); + assert.match(error.errors[0].message, /fixture construction failed/); + assert.match(error.errors[1].message, /resource cleanup failed/); + return true; + }); + assert.equal(state.events.includes('query:1:DROP'), true); + assert.equal(state.events.includes('end:1'), true); + } + process.stdout.write('managed-resource-phase-fakes-complete\n'); + })().catch(error => { console.error(error); process.exitCode = 1; }); + """.replace( + "__MANAGED__", json.dumps(str(DATA / "ts_managed_reporting_server.cjs")) + ) + completed = subprocess.run( + ["node", "-e", script], + check=True, + cwd=ROOT, + capture_output=True, + text=True, + timeout=30, + ) + assert completed.stdout.splitlines() == ["managed-resource-phase-fakes-complete"] + + +def test_webhook_receiver_result_disposition_cannot_promote_a_historical_red() -> None: + client = load_python_module( + "reporting_interop_webhook_receiver_client", + DATA / "signing" / "webhook_receiver_client.py", + ) + + assert client._disposition( + normative_match=False, + receiver_http_mapping_match=False, + historical_red_reproduced=True, + allow_historical_red=False, + ) == ("blocking_red", False, 1) + assert client._disposition( + normative_match=False, + receiver_http_mapping_match=False, + historical_red_reproduced=True, + allow_historical_red=True, + ) == ("historical_red_reproduced", False, 0) + assert client._disposition( + normative_match=True, + receiver_http_mapping_match=False, + historical_red_reproduced=False, + allow_historical_red=False, + ) == ("blocking_red", False, 1) + assert client._disposition( + normative_match=True, + receiver_http_mapping_match=True, + historical_red_reproduced=False, + allow_historical_red=False, + ) == ("passed", True, 0) + assert client._disposition( + normative_match=True, + receiver_http_mapping_match=True, + historical_red_reproduced=False, + allow_historical_red=True, + ) == ("historical_red_not_reproduced", False, 1) + assert client._disposition( + normative_match=False, + receiver_http_mapping_match=False, + historical_red_reproduced=False, + allow_historical_red=True, + ) == ("historical_red_not_reproduced", False, 1) + + +def test_typescript_semantic_reconciliation_requires_managed_evidence() -> None: + corpus = load("corpus.json") + applicability = load("applicability.json") + scenario = next( + row + for row in corpus["scenarios"] + if row["id"] == "foundation.reconciliation.typescript_public" + ) + override = next(row for row in applicability["overrides"] if row["scenario"] == scenario["id"]) + + assert scenario["requires_server_tier"] == "managed_delivery" + assert override["requires_server_tier"] == "managed_delivery" + + +def test_official_precedence_probe_inputs_preserve_supplied_bytes() -> None: + expected = { + "fixture.json": "dc16c44297a315122c4450bdb4c7278b5635894f7a60fb0266456b8d083da013", + "pins.json": "a56a77f6db242704d134af7f1c7a514b3f904f006180744ad3123716ce7a9ad1", + "repro-rc42.cjs": "56b5540918fcedf23e7ff2035708a3b0ccef909f4afd72bf8a4ea2d1630bfdef", + "cases.json": "fd7c0167a45da95b6c756f85cc39b7845210d1a929ecc0d6ef0a10a5b658c100", + } + root = DATA / "official_precedence" + + assert (root / "fixture.json").stat().st_size == 9908 + assert (root / "fixture.json").read_bytes().endswith(b"\n") + assert not (root / "fixture.json").read_bytes().endswith(b"\n\n") + for name, digest in expected.items(): + assert hashlib.sha256((root / name).read_bytes()).hexdigest() == digest + + +def test_official_precedence_candidate_adapter_is_separate_and_exact() -> None: + root = DATA / "official_precedence" + candidate = json.loads((root / "pins-rc45.json").read_text()) + adapter = (root / "repro-candidate.cjs").read_text() + + assert candidate["version"] == "14.0.0-rc.45" + assert candidate["tarballSha256"] == ( + "a0952ed8edaaad958bdb8f474c4f5cb68333ee272e7a8f3419d12930e9c57e6b" + ) + assert candidate["fixtureSha256"] == ( + "dc16c44297a315122c4450bdb4c7278b5635894f7a60fb0266456b8d083da013" + ) + assert "require('./pins.json')" in adapter + assert "require('./pins-rc45.json')" in adapter + assert "require('./repro-rc42.cjs')" in adapter + + +def test_transport_header_defect_stays_visible_nonblocking() -> None: + corpus = load("corpus.json") + applicability = load("applicability.json") + scenario_id = "foundation.transport.response_headers_metadata" + + assert any(row["id"] == scenario_id for row in corpus["scenarios"]) + override = next(row for row in applicability["overrides"] if row["scenario"] == scenario_id) + assert override["all"] == "visible_nonblocking" + + +def test_official_precedence_manifest_binds_every_case_identity_and_expectation() -> None: + manifest = json.loads((DATA / "official_precedence" / "cases.json").read_text()) + cases = manifest["cases"] + + assert manifest["protocol_owned"] is False + assert manifest["scope"] == "supplemental_shared_neutral_regression" + assert len(cases) == 17 + assert len({row["id"] for row in cases}) == 17 + assert cases[-1]["id"] == "official_precedence/single_official" + matrix = cases[:-1] + assert {tuple(row["id"].removeprefix("official_precedence/").split("/")) for row in matrix} == { + (finality, artifact, linked, order) + for finality in ("snapshot", "official") + for artifact in ("both_artifacts", "official_without_materialization") + for linked in ("unlinked", "linked") + for order in ("natural", "reversed") + } + assert all("reportingRevisionId=revision-august-official" in row["required"] for row in cases) diff --git a/tests/test_reporting_interop_matrix.py b/tests/test_reporting_interop_matrix.py new file mode 100644 index 000000000..3cfa7eeda --- /dev/null +++ b/tests/test_reporting_interop_matrix.py @@ -0,0 +1,287 @@ +from __future__ import annotations + +import copy +import hashlib +import importlib.util +import json +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "ci" / "reporting_interop_matrix.py" +MANIFEST = SCRIPT.with_name("reporting_interop_inputs.json") +SPEC = importlib.util.spec_from_file_location("reporting_interop_matrix", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +matrix = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = matrix +SPEC.loader.exec_module(matrix) + + +def inputs() -> dict: + return json.loads(MANIFEST.read_text(encoding="utf-8")) + + +def test_preparation_manifest_keeps_exact_required_cross_product() -> None: + result = matrix.validate_manifest(inputs(), acceptance=False) + + assert result.ready is False + assert result.missing == ("python.stable", "python.candidate", "typescript.candidate") + assert [ + tuple(cell[key] for key in ("id", "python", "typescript", "protocol")) + for cell in inputs()["cells"] + ] == list(matrix.EXPECTED_CELLS) + assert all(cell["required"] is True for cell in inputs()["cells"]) + + +def test_acceptance_fails_closed_while_coordinates_are_missing() -> None: + with pytest.raises(matrix.InputError, match="acceptance inputs are incomplete"): + matrix.validate_manifest(inputs(), acceptance=True) + + +@pytest.mark.parametrize("mutation", ["delete", "optional", "wrong_protocol", "duplicate"]) +def test_controlling_cells_cannot_be_weakened(mutation: str) -> None: + value = inputs() + if mutation == "delete": + value["cells"].pop() + elif mutation == "optional": + value["cells"][1]["required"] = False + elif mutation == "wrong_protocol": + value["cells"][2]["protocol"] = "candidate" + else: + value["cells"][3] = copy.deepcopy(value["cells"][0]) + + with pytest.raises(matrix.InputError): + matrix.validate_manifest(value, acceptance=False) + + +def test_mutable_or_source_only_inputs_are_rejected() -> None: + typescript = copy.deepcopy(inputs()["artifacts"]["typescript"]["stable"]) + typescript["version"] = "latest" + with pytest.raises(matrix.InputError, match="exact package version"): + matrix._validate_typescript("stable", typescript) + + python = { + "package": "adcp", + "version": "8.0.0rc1", + "registry": "https://pypi.example.invalid/", + "wheel_url": "https://example.invalid/adcp-8.0.0rc1-py3-none-any.whl", + "wheel_sha256": "a" * 64, + "protocol": "3.2.0-rc.4", + "source_kind": "pr", + } + with pytest.raises(matrix.InputError, match="source-only"): + matrix._validate_python("candidate", python) + + +def test_rc41_cannot_be_substituted_for_corrected_candidate() -> None: + value = inputs() + pending = value["pending"]["typescript_candidate"] + pending["proposed_version"] = pending["forbidden_substitute"] + + with pytest.raises(matrix.InputError, match="forbidden prior release"): + matrix.monitor_typescript(value) + + candidate = copy.deepcopy(value["artifacts"]["typescript"]["stable"]) + with pytest.raises(matrix.InputError, match="cannot substitute"): + matrix._validate_typescript("candidate", candidate) + + +def test_npm_integrity_must_be_sha512() -> None: + typescript = copy.deepcopy(inputs()["artifacts"]["typescript"]["stable"]) + typescript["integrity"] = "sha256-deadbeef" + + with pytest.raises(matrix.InputError, match="must be sha512"): + matrix._validate_typescript("stable", typescript) + + +def test_baseline_is_explicitly_non_acceptance() -> None: + baseline = inputs()["baseline_inputs"]["python_registry"] + + assert baseline["version"] == "8.0.0b15" + assert baseline["protocol"] == "3.2.0-rc.3" + assert baseline["acceptance"] is False + + +def complete_inputs() -> dict: + value = inputs() + value["phase"] = "acceptance" + seller_entrypoint = SCRIPT.relative_to(ROOT) + buyer_entrypoint = Path(__file__).resolve().relative_to(ROOT) + value["execution"].update( + { + "postgresql_version": "16.14", + "os_release_sha256": hashlib.sha256(Path("/etc/os-release").read_bytes()).hexdigest(), + "seller_entrypoint": str(seller_entrypoint), + "seller_entrypoint_sha256": hashlib.sha256(SCRIPT.read_bytes()).hexdigest(), + "buyer_entrypoint": str(buyer_entrypoint), + "buyer_entrypoint_sha256": hashlib.sha256( + Path(__file__).resolve().read_bytes() + ).hexdigest(), + } + ) + for role, version, digest, protocol in ( + ("stable", "8.0.0rc1", "1" * 64, "3.2.0-rc.3"), + ("candidate", "8.0.0rc2", "2" * 64, "3.2.0-rc.4"), + ): + value["artifacts"]["python"][role] = { + "package": "adcp", + "version": version, + "registry": "https://pypi.example.invalid/", + "wheel_url": f"https://packages.example.invalid/adcp-{version}-py3-none-any.whl", + "wheel_sha256": digest, + "protocol": protocol, + "python_version": "3.12.14", + "requirements_lock_url": ( + f"https://packages.example.invalid/adcp-{version}-requirements.txt" + ), + "requirements_lock_sha256": ("7" if role == "stable" else "8") * 64, + } + candidate = copy.deepcopy(value["artifacts"]["typescript"]["stable"]) + candidate["version"] = "14.0.0-rc.42" + candidate["tarball_url"] = "https://registry.npmjs.org/@adcp/sdk/-/sdk-14.0.0-rc.42.tgz" + candidate["shasum"] = "4" * 40 + value["artifacts"]["typescript"]["candidate"] = candidate + for role in ("stable", "candidate"): + item = value["artifacts"]["typescript"][role] + item.update( + { + "node_version": "24.19.0", + "npm_version": "10.9.4", + "lock_url": f"https://packages.example.invalid/sdk-{item['version']}-package-lock.json", + "lock_sha256": ("9" if role == "stable" else "a") * 64, + } + ) + return value + + +def retained(path: Path, root: Path) -> dict: + data = path.read_bytes() + return { + "path": str(path.relative_to(root)), + "bytes": len(data), + "sha256": hashlib.sha256(data).hexdigest(), + } + + +def passing_results(value: dict, run_root: Path) -> dict: + aggregate = run_root / "aggregate.log" + aggregate.write_text("four required cells passed\n", encoding="utf-8") + cells = [] + for index, expected in enumerate(value["cells"]): + log = run_root / f"cell-{index}.log" + log.write_text(f"cell {index}\n", encoding="utf-8") + python = value["artifacts"]["python"][expected["python"]] + typescript = value["artifacts"]["typescript"][expected["typescript"]] + protocol = value["protocols"][expected["protocol"]] + cells.append( + { + "id": expected["id"], + "status": "passed", + "acceptance": True, + "seller_pid": 1000 + index, + "database": f"reporting_interop_{index}", + "state_directory": f"state/{index}", + "assertions": {name: True for name in matrix.REQUIRED_ASSERTIONS}, + "inputs": { + "python": { + "version": python["version"], + "wheel_sha256": python["wheel_sha256"], + "requirements_lock_sha256": python["requirements_lock_sha256"], + }, + "typescript": { + "version": typescript["version"], + "integrity": typescript["integrity"], + "lock_sha256": typescript["lock_sha256"], + }, + "protocol": { + "version": protocol["version"], + "sha256": protocol["sha256"], + }, + }, + "runtime": { + "python": python["python_version"], + "node": typescript["node_version"], + "npm": typescript["npm_version"], + "postgresql": value["execution"]["postgresql_version"], + "os_release_sha256": value["execution"]["os_release_sha256"], + "database_encoding": "UTF8", + "database_collation": "C", + }, + "evidence": [retained(log, run_root)], + } + ) + return { + "acceptance": True, + "status": "passed", + "cells": cells, + "evidence": [retained(aggregate, run_root)], + } + + +def test_result_gate_checks_all_semantics_and_cross_cell_isolation(tmp_path: Path) -> None: + value = complete_inputs() + results = passing_results(value, tmp_path) + + matrix.validate_results(value, results, run_root=tmp_path) + + results["cells"][3]["database"] = results["cells"][0]["database"] + with pytest.raises(matrix.InputError, match="isolated database"): + matrix.validate_results(value, results, run_root=tmp_path) + + +def test_acceptance_requires_dependency_locks_and_exact_runtimes() -> None: + value = complete_inputs() + del value["artifacts"]["python"]["candidate"]["requirements_lock_sha256"] + + with pytest.raises(matrix.InputError, match="requirements_lock_sha256"): + matrix.validate_manifest(value, acceptance=True) + + +def test_result_gate_rejects_runtime_drift(tmp_path: Path) -> None: + value = complete_inputs() + results = passing_results(value, tmp_path) + results["cells"][1]["runtime"]["node"] = "24.19.1" + + with pytest.raises(matrix.InputError, match="runtime identity"): + matrix.validate_results(value, results, run_root=tmp_path) + + +def test_result_gate_rejects_one_missing_semantic_assertion(tmp_path: Path) -> None: + value = complete_inputs() + results = passing_results(value, tmp_path) + del results["cells"][2]["assertions"]["account_activity_retry_visible"] + + with pytest.raises(matrix.InputError, match="every required semantic assertion"): + matrix.validate_results(value, results, run_root=tmp_path) + + +def test_legacy_validate_results_command_is_explicitly_nonaccepting( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + value = complete_inputs() + results = passing_results(value, tmp_path) + manifest_path = tmp_path / "manifest.json" + results_path = tmp_path / "results.json" + manifest_path.write_text(json.dumps(value), encoding="utf-8") + results_path.write_text(json.dumps(results), encoding="utf-8") + + assert ( + matrix.main( + [ + "--manifest", + str(manifest_path), + "validate-results", + "--results", + str(results_path), + "--run-root", + str(tmp_path), + ] + ) + == 0 + ) + emitted = json.loads(capsys.readouterr().out) + assert emitted["acceptance"] is False + assert emitted["blocking_acceptance"] is False + assert emitted["status"] == "legacy_result_shape_validated_nonaccepting" diff --git a/tests/test_reporting_storyboard_orchestration.py b/tests/test_reporting_storyboard_orchestration.py new file mode 100644 index 000000000..333d57caa --- /dev/null +++ b/tests/test_reporting_storyboard_orchestration.py @@ -0,0 +1,1740 @@ +from __future__ import annotations + +import hashlib +import importlib.util +import io +import json +import signal +import subprocess +import sys +import tarfile +import types +import zipfile +from pathlib import Path +from types import SimpleNamespace + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +MODULE_PATH = ROOT / "scripts/ci/reporting_interop/storyboard_orchestration.py" +SPEC = importlib.util.spec_from_file_location("reporting_storyboard_orchestration", MODULE_PATH) +assert SPEC is not None and SPEC.loader is not None +orchestration = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = orchestration +SPEC.loader.exec_module(orchestration) + + +def inventory() -> dict: + rows = [] + for storyboard_id, steps, stateful, _server_kind in orchestration.REQUIRED_STORYBOARDS: + rows.append( + { + "id": storyboard_id, + "step_count": steps, + "stateful_step_count": stateful, + "steps": [ + {"id": f"{storyboard_id}-{index}", "title": f"step {index}", "task": "task"} + for index in range(steps) + ], + } + ) + return {"inventory": rows, "totals": dict(orchestration.EXPECTED_TOTALS)} + + +def test_inventory_requires_all_five_storyboards_in_exact_order() -> None: + value = inventory() + cases = orchestration.validate_inventory(value) + assert [case.storyboard_id for case in cases] == [ + item[0] for item in orchestration.REQUIRED_STORYBOARDS + ] + + value["inventory"].pop() + with pytest.raises(orchestration.OrchestrationError, match="exactly 5 rows"): + orchestration.validate_inventory(value) + + value = inventory() + value["inventory"].append(dict(value["inventory"][-1])) + with pytest.raises(orchestration.OrchestrationError, match="exactly 5 rows"): + orchestration.validate_inventory(value) + + value = inventory() + value["inventory"].reverse() + with pytest.raises(orchestration.OrchestrationError, match="exact ordered denominator"): + orchestration.validate_inventory(value) + + +def test_inventory_rejects_count_and_literal_identity_drift() -> None: + value = inventory() + value["inventory"][0]["steps"][1]["id"] = value["inventory"][0]["steps"][0]["id"] + with pytest.raises(orchestration.OrchestrationError, match="missing or duplicate"): + orchestration.validate_inventory(value) + + value = inventory() + value["totals"]["steps"] -= 1 + with pytest.raises(orchestration.OrchestrationError, match="totals drifted"): + orchestration.validate_inventory(value) + + +def test_external_dsn_is_explicit_and_loopback_by_default() -> None: + orchestration.validate_external_dsn( + "postgresql://127.0.0.1:5432/postgres", allow_non_loopback=False + ) + with pytest.raises(orchestration.OrchestrationError, match="must be loopback"): + orchestration.validate_external_dsn( + "postgresql://database.example.test/postgres", allow_non_loopback=False + ) + orchestration.validate_external_dsn( + "postgresql://database.example.test/postgres", allow_non_loopback=True + ) + + +def test_wrong_node_or_package_identity_fails_before_execution(tmp_path: Path) -> None: + node = tmp_path / "node" + node.write_bytes(b"not-the-pinned-node") + node.chmod(0o755) + install = tmp_path / "install" + (install / "node_modules/@adcp/sdk/bin").mkdir(parents=True) + (install / "package-lock.json").write_text("{}\n", encoding="utf-8") + (install / "node_modules/@adcp/sdk/package.json").write_text( + '{"version":"14.0.0-rc.45"}\n', encoding="utf-8" + ) + (install / "node_modules/@adcp/sdk/bin/adcp.js").write_text("", encoding="utf-8") + inventory_path = tmp_path / "inventory.json" + inventory_path.write_text(json.dumps(inventory()), encoding="utf-8") + args = SimpleNamespace( + node_runtime=node, + node_sha256="0" * 64, + typescript_install=install, + typescript_tarball=tmp_path / "sdk.tgz", + typescript_role="candidate", + inventory_json=inventory_path, + external_pg_admin_dsn="postgresql://127.0.0.1/postgres", + allow_non_loopback_external_dsn=False, + local_pg_bin=None, + service_port_base=55_500, + ) + with pytest.raises(orchestration.OrchestrationError, match="Node runtime hash mismatch"): + orchestration.validate_fixed_inputs(args) + + args.node_sha256 = orchestration.sha256(node) + with pytest.raises( + orchestration.OrchestrationError, + match="rc.6 candidate identity.*no selected executable lock/member binding", + ): + orchestration.validate_fixed_inputs(args) + + +class FakeProcess: + def __init__(self, pid: int = 4312, *, timeout_once: bool = False) -> None: + self.pid = pid + self.returncode = None + self.wait_timeouts: list[float] = [] + self.timeout_once = timeout_once + + def poll(self): + return self.returncode + + def wait(self, timeout: float): + self.wait_timeouts.append(timeout) + if self.timeout_once: + self.timeout_once = False + raise subprocess.TimeoutExpired("fake", timeout) + self.returncode = 0 + return 0 + + +def test_owned_process_never_signals_a_reused_pid() -> None: + process = FakeProcess() + signals: list[tuple[int, int]] = [] + original = orchestration.ProcessIdentity(process.pid, "S", process.pid, process.pid, "10") + reused = orchestration.ProcessIdentity(process.pid, "S", process.pid, process.pid, "20") + owned = orchestration.OwnedProcess( + process, + identity=original, + identity_reader=lambda _pid: reused, + member_reader=lambda _session, _start: (reused,), + pidfd_open=lambda pid: pid, + pidfd_signal=lambda pid, sig: signals.append((pid, sig)), + pidfd_close=lambda _fd: None, + ) + with pytest.raises(orchestration.OrchestrationError, match="unowned PID"): + owned.stop(1) + assert signals == [] + + +def test_owned_process_escalates_only_exact_owned_session_members() -> None: + class StubbornProcess(FakeProcess): + def wait(self, timeout: float): + self.wait_timeouts.append(timeout) + if self.returncode is None: + raise subprocess.TimeoutExpired("fake", timeout) + return self.returncode + + process = StubbornProcess() + signals: list[tuple[int, int]] = [] + leader = orchestration.ProcessIdentity(process.pid, "S", process.pid, process.pid, "10") + child = orchestration.ProcessIdentity(process.pid + 1, "S", process.pid, process.pid, "11") + + def signal_member(pid: int, sig: int) -> None: + signals.append((pid, sig)) + if sig == signal.SIGKILL and pid == process.pid: + process.returncode = -signal.SIGKILL + + owned = orchestration.OwnedProcess( + process, + identity=leader, + identity_reader=lambda pid: leader if pid == leader.pid else child, + member_reader=lambda _session, _start: ( + () if process.returncode is not None else (child, leader) + ), + pidfd_open=lambda pid: pid, + pidfd_signal=signal_member, + pidfd_close=lambda _fd: None, + ) + owned.stop(0) + assert signals == [ + (child.pid, signal.SIGTERM), + (leader.pid, signal.SIGTERM), + (child.pid, signal.SIGKILL), + (leader.pid, signal.SIGKILL), + ] + assert owned.death_proven is True + + +def test_owned_process_settles_descendant_after_direct_child_exit() -> None: + process = FakeProcess() + process.returncode = 0 + leader = orchestration.ProcessIdentity(process.pid, "Z", process.pid, process.pid, "10") + child = orchestration.ProcessIdentity(process.pid + 1, "S", process.pid, process.pid, "11") + child_alive = True + signals: list[tuple[int, int]] = [] + + def members(_session: int, _start: str): + return (child,) if child_alive else () + + def signal_member(pid: int, sig: int) -> None: + nonlocal child_alive + signals.append((pid, sig)) + child_alive = False + + owned = orchestration.OwnedProcess( + process, + identity=leader, + identity_reader=lambda _pid: child, + member_reader=members, + pidfd_open=lambda pid: pid, + pidfd_signal=signal_member, + pidfd_close=lambda _fd: None, + ) + owned.stop(1) + assert signals == [(child.pid, signal.SIGTERM)] + assert owned.death_proven is True + + +def test_owned_temp_cleanup_requires_the_original_marker(tmp_path: Path) -> None: + owned = orchestration.OwnedTempRoot(tmp_path) + retained = owned.path / "retained.txt" + retained.write_text("evidence", encoding="utf-8") + owned.marker.write_text('{"token":"different"}\n', encoding="utf-8") + with pytest.raises(orchestration.OrchestrationError, match="mismatched owner"): + owned.cleanup() + assert retained.is_file() + + owned.marker.write_text(json.dumps({"token": owned.token}) + "\n", encoding="utf-8") + owned.cleanup() + assert not owned.path.exists() + + +def test_local_postgres_init_failure_removes_only_its_owned_temp_root( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(orchestration, "_run_owned_to_files", lambda *args, **kwargs: 1) + lifecycle = orchestration.LocalPostgres( + pg_bin=tmp_path / "bin", + port=55_439, + output=tmp_path, + startup_timeout=1, + shutdown_timeout=1, + ) + with pytest.raises(orchestration.OrchestrationError, match="initdb failed"): + lifecycle.start() + assert lifecycle.temp is not None + assert not lifecycle.temp.path.exists() + + +def test_spawn_preflight_fails_before_popen(monkeypatch: pytest.MonkeyPatch) -> None: + invoked: list[bool] = [] + monkeypatch.setattr( + orchestration, + "assert_process_ownership_supported", + lambda: (_ for _ in ()).throw(orchestration.OrchestrationError("unsupported")), + ) + with pytest.raises(orchestration.OrchestrationError, match="unsupported"): + orchestration.OwnedProcess.spawn( + ["never"], ownership_timeout=1, popen=lambda *_args, **_kwargs: invoked.append(True) + ) + assert invoked == [] + + +def test_spawn_proc_identity_failure_settles_child_through_pidfd( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(orchestration, "assert_process_ownership_supported", lambda: None) + process = FakeProcess() + read_fd, write_fd = orchestration.os.pipe() + signals: list[int] = [] + try: + with pytest.raises(orchestration.OrchestrationError, match="safely settled"): + orchestration.OwnedProcess.spawn( + ["fake"], + ownership_timeout=1, + popen=lambda *_args, **_kwargs: process, + pidfd_open=lambda _pid: read_fd, + pidfd_signal=lambda _fd, sig: signals.append(sig), + identity_reader=lambda _pid: None, + ) + assert signals == [signal.SIGKILL] + assert process.returncode == 0 + finally: + orchestration.os.close(write_fd) + + +def test_spawn_uses_a_gated_launcher_before_releasing_target( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(orchestration, "assert_process_ownership_supported", lambda: None) + process = FakeProcess() + identity = orchestration.ProcessIdentity(process.pid, "S", process.pid, process.pid, "10") + captured: dict[str, object] = {} + inherited_gate: list[int] = [] + + def popen(command, **kwargs): + captured["command"] = command + captured["pass_fds"] = kwargs["pass_fds"] + inherited_gate.append(orchestration.os.dup(kwargs["pass_fds"][0])) + return process + + owned = orchestration.OwnedProcess.spawn( + ["target", "--flag"], + ownership_timeout=1, + popen=popen, + identity_reader=lambda _pid: identity, + member_reader=lambda _session, _start: (), + pidfd_open=lambda _pid: 77, + pidfd_signal=lambda _fd, _sig: None, + pidfd_close=lambda _fd: None, + start_new_session=True, + ) + assert captured["command"][:3] == [sys.executable, "-I", "-c"] + assert "os.execvpe" in captured["command"][3] + assert json.loads(captured["command"][5]) == ["target", "--flag"] + assert len(captured["pass_fds"]) == 1 + orchestration.os.close(inherited_gate.pop()) + process.returncode = 0 + owned.stop(1) + + +def test_postgres_stop_failure_retains_owned_data(tmp_path: Path) -> None: + lifecycle = orchestration.LocalPostgres( + pg_bin=tmp_path / "bin", + port=55_439, + output=tmp_path, + startup_timeout=1, + shutdown_timeout=1, + ) + lifecycle.temp = orchestration.OwnedTempRoot(tmp_path) + retained = lifecycle.temp.path + running = FakeProcess() + + class FailedStop: + process = running + + def stop(self, _timeout): + raise orchestration.OrchestrationError("death unproven") + + lifecycle.process = FailedStop() + with pytest.raises(orchestration.OrchestrationError, match=str(retained)): + lifecycle.close() + assert retained.is_dir() + + +def test_postgres_stop_failure_retains_data_even_if_direct_child_exited(tmp_path: Path) -> None: + lifecycle = orchestration.LocalPostgres( + pg_bin=tmp_path / "bin", + port=55_439, + output=tmp_path, + startup_timeout=1, + shutdown_timeout=1, + ) + lifecycle.temp = orchestration.OwnedTempRoot(tmp_path) + retained = lifecycle.temp.path + exited = FakeProcess() + exited.returncode = 0 + + class FailedDescendantProof: + process = exited + + def stop(self, _timeout): + raise orchestration.OrchestrationError("descendant death unproven") + + lifecycle.process = FailedDescendantProof() + with pytest.raises(orchestration.OrchestrationError, match=str(retained)): + lifecycle.close() + assert retained.is_dir() + + +def test_failure_propagates_stops_later_cases_and_cleanup_always_runs() -> None: + cases = [ + orchestration.Case("one", 1, 1, "controlled"), + orchestration.Case("two", 1, 1, "controlled"), + orchestration.Case("three", 1, 1, "controlled"), + ] + invoked: list[str] = [] + cleaned: list[bool] = [] + + def runner(_index, case): + invoked.append(case.storyboard_id) + return {"id": case.storyboard_id, "fully_executed": case.storyboard_id == "one"} + + rows, errors = orchestration.execute_case_sequence(cases, runner, lambda: cleaned.append(True)) + assert [row["id"] for row in rows] == ["one", "two"] + assert invoked == ["one", "two"] + assert cleaned == [True] + assert errors == ["two: storyboard did not fully execute"] + + +def test_cleanup_failure_is_blocking_even_after_success() -> None: + case = orchestration.Case("one", 1, 1, "controlled") + + def cleanup(): + raise RuntimeError("drop failed") + + rows, errors = orchestration.execute_case_sequence( + [case], lambda _index, _case: {"id": "one", "fully_executed": True}, cleanup + ) + assert rows == [{"id": "one", "fully_executed": True}] + assert errors == ["cleanup: drop failed"] + + +def test_storyboard_timeout_retains_partial_output_and_settles_child( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + process = FakeProcess(timeout_once=True) + stopped: list[float] = [] + + class Owned: + def __init__(self, stdout): + self.process = process + stdout.write(b"partial-json") + stdout.flush() + + def stop(self, timeout): + stopped.append(timeout) + process.wait(timeout) + + monkeypatch.setattr( + orchestration.OwnedProcess, + "spawn", + lambda *_args, **kwargs: Owned(kwargs["stdout"]), + ) + stdout = tmp_path / "stdout.json" + stderr = tmp_path / "stderr.log" + with pytest.raises(orchestration.OrchestrationError, match="partial output retained"): + orchestration._run_owned_to_files( + ["fake"], + cwd=tmp_path, + env={}, + stdout_path=stdout, + stderr_path=stderr, + timeout=1, + shutdown_timeout=2, + ) + assert stdout.read_bytes() == b"partial-json" + assert stopped == [2] + assert process.returncode == 0 + + +def test_storyboard_timeout_reports_execution_and_cleanup_failures_separately( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + process = FakeProcess(timeout_once=True) + + class Owned: + def __init__(self): + self.process = process + + def stop(self, _timeout): + raise orchestration.OrchestrationError("descendants remain") + + monkeypatch.setattr(orchestration.OwnedProcess, "spawn", lambda *_args, **_kwargs: Owned()) + with pytest.raises( + orchestration.OrchestrationError, + match="execution failed: command timed out.*cleanup failed: descendants remain", + ): + orchestration._run_owned_to_files( + ["fake"], + cwd=tmp_path, + env={}, + stdout_path=tmp_path / "stdout.json", + stderr_path=tmp_path / "stderr.log", + timeout=1, + shutdown_timeout=2, + ) + + +def test_stale_ready_file_cannot_prove_owned_seller(tmp_path: Path) -> None: + process = FakeProcess(pid=7182) + owned = SimpleNamespace(process=process, pid=process.pid) + ready = tmp_path / "ready.json" + ready.write_text( + json.dumps({"startup_proof_sha256": "stale", "pid": process.pid, "port": 55_500}), + encoding="utf-8", + ) + with pytest.raises(orchestration.OrchestrationError, match="startup_proof_sha256 mismatch"): + orchestration._wait_owned_server( + owned, + ready, + { + "startup_proof_sha256": "fresh", + "pid": process.pid, + "port": 55_500, + "auth_binding_sha256": "a" * 64, + "seller_package": {}, + "node": {"executable": "/node"}, + }, + 0.01, + ) + + +def test_seller_readiness_rejects_misrouted_package_runtime_and_auth() -> None: + expected = { + "pid": 101, + "port": 55_500, + "startup_proof_sha256": "a" * 64, + "auth_binding_sha256": "a" * 64, + "seller_package": { + "name": "@adcp/sdk", + "version": "14.0.0-rc.47", + "integrity": "sha512-expected", + "adcp_version": "3.2.0-rc.6", + }, + "node": {"executable": "/owned/node"}, + } + ready = { + **expected, + "node": {"executable": "/owned/node", "version": "v22.12.0"}, + } + orchestration._validate_seller_readiness(ready, expected) + + for replacement, message in [ + ({"auth_binding_sha256": "b" * 64}, "auth_binding_sha256 mismatch"), + ( + {"seller_package": {**expected["seller_package"], "integrity": "sha512-stale"}}, + "package identity mismatch", + ), + ( + {"node": {"executable": "/stale/node", "version": "v22.12.0"}}, + "runtime executable mismatch", + ), + ]: + with pytest.raises(orchestration.OrchestrationError, match=message): + orchestration._validate_seller_readiness({**ready, **replacement}, expected) + + +def test_storyboard_private_inputs_are_supplied_outside_every_server_argv( + tmp_path: Path, +) -> None: + inputs = orchestration.FixedInputs( + role="candidate", + node=tmp_path / "node", + node_sha256="a" * 64, + install=tmp_path / "install", + tarball=tmp_path / "sdk.tgz", + tarball_sha256="b" * 64, + package_member_count=1, + package_member_manifest_sha256="c" * 64, + lock=tmp_path / "package-lock.json", + lock_sha256="d" * 64, + version="14.0.0-rc.47", + integrity="sha512-example", + adcp_version="3.2.0-rc.6", + inventory=inventory(), + cases=orchestration.validate_inventory(inventory()), + ) + token = "a" * 43 + proof = "b" * 64 + for case, database_url in [ + (orchestration.Case("reporting_core", 23, 23, "controlled"), None), + ( + orchestration.Case("reliable_reporting_managed_delivery", 6, 5, "managed"), + "postgresql://fixture/db", + ), + ( + orchestration.Case("reliable_reporting_reconciled_billing", 16, 15, "billing"), + "postgresql://fixture/db", + ), + ]: + command = orchestration._server_command( + inputs, + case, + port=55_500, + database_url=database_url, + ready_file=tmp_path / f"{case.storyboard_id}.ready.json", + ) + assert token not in command and proof not in command + assert "--auth-token" not in command and "--startup-proof" not in command + environment = orchestration._server_environment(token, proof) + assert environment[orchestration.STORYBOARD_PRIVATE_ENV["auth_token"]] == token + assert environment[orchestration.STORYBOARD_PRIVATE_ENV["startup_proof"]] == proof + + +def test_storyboard_cli_handoff_uses_supported_auth_environment( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + inputs = orchestration.FixedInputs( + role="candidate", + node=tmp_path / "node", + node_sha256="a" * 64, + install=tmp_path / "install", + tarball=tmp_path / "sdk.tgz", + tarball_sha256="b" * 64, + package_member_count=1, + package_member_manifest_sha256="c" * 64, + lock=tmp_path / "package-lock.json", + lock_sha256="d" * 64, + version="14.0.0-rc.47", + integrity="sha512-example", + adcp_version="3.2.0-rc.6", + inventory=inventory(), + cases=orchestration.validate_inventory(inventory()), + ) + case = orchestration.Case("unit_storyboard", 1, 1, "controlled") + inventory_row = { + "id": case.storyboard_id, + "steps": [{"id": "step-1", "title": "step one", "task": "unit_task"}], + } + output = tmp_path / "run" + output.mkdir() + captured: dict[str, object] = {} + server_environment: dict[str, str] = {} + stopped: list[bool] = [] + + class FakeOwned: + pid = 4242 + identity = SimpleNamespace(start_token="123") + + def stop(self, _timeout: float) -> None: + stopped.append(True) + + def record_server(_command, **kwargs): + server_environment.update(kwargs["env"]) + return FakeOwned() + + monkeypatch.setattr(orchestration.OwnedProcess, "spawn", record_server) + monkeypatch.setattr( + orchestration, + "_wait_owned_server", + lambda *_args, **_kwargs: {"kind": "owned-ready"}, + ) + monkeypatch.setattr(orchestration, "_minimal_environment", lambda: {"PATH": "/runtime"}) + + def record_cli(command, **kwargs): + captured["command"] = list(command) + captured["environment"] = dict(kwargs["env"]) + kwargs["stdout_path"].write_text( + json.dumps( + { + "storyboards_executed": [case.storyboard_id], + "tracks": [ + { + "scenarios": [ + { + "steps": [ + { + "step": "step one", + "task": "unit_task", + "passed": True, + } + ] + } + ] + } + ], + } + ), + encoding="utf-8", + ) + return 0 + + monkeypatch.setattr(orchestration, "_run_owned_to_files", record_cli) + result = orchestration._run_case( + inputs, + case, + inventory_row, + index=0, + port=55_500, + database_url=None, + output=output, + startup_timeout=1, + storyboard_timeout=1, + shutdown_timeout=1, + ) + + command = captured["command"] + environment = captured["environment"] + assert isinstance(command, list) and "--auth" not in command + assert isinstance(environment, dict) + assert environment == { + "PATH": "/runtime", + "ADCP_ALLOW_INTERNAL_PROBES": "1", + "ADCP_AUTH_TOKEN": environment["ADCP_AUTH_TOKEN"], + } + assert ( + environment["ADCP_AUTH_TOKEN"] + == server_environment[orchestration.STORYBOARD_PRIVATE_ENV["auth_token"]] + ) + assert environment["ADCP_AUTH_TOKEN"] not in command + assert result["fully_executed"] is True + assert stopped == [True] + + +def test_uncertain_create_is_retained_for_exact_name_cleanup( + monkeypatch: pytest.MonkeyPatch, +) -> None: + owner = orchestration.DatabaseOwner("postgresql://127.0.0.1/postgres", "abc123", timeout=1) + statements: list[object] = [] + create_attempts = 0 + + class Connection: + def __enter__(self): + return self + + def __exit__(self, *_args): + return False + + def execute(self, statement): + nonlocal create_attempts + statements.append(statement) + if create_attempts == 0: + create_attempts += 1 + raise RuntimeError("transport outcome unknown") + return SimpleNamespace(fetchone=lambda: None) + + class SQL: + def __init__(self, value): + self.value = value + + def format(self, *_args): + return self + + fake_sql = SimpleNamespace(SQL=SQL, Identifier=lambda value: value) + monkeypatch.setitem(sys.modules, "psycopg", SimpleNamespace(sql=fake_sql)) + + monkeypatch.setattr(owner, "_connect", lambda *_args, **_kwargs: Connection()) + with pytest.raises(RuntimeError, match="outcome unknown"): + owner.create(4) + assert owner.created == ["adcp_story_abc123_4"] + owner.close() + assert len(statements) == 2 + + +def test_postgres_started_then_setup_failed_still_runs_cleanup( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + closed: list[bool] = [] + + class Local: + identity = {"data_directory": "/owned"} + + def __init__(self, **_kwargs): + pass + + def start(self): + return "postgresql://127.0.0.1/postgres" + + def close(self): + closed.append(True) + + class Owner: + def __init__(self, *_args, **_kwargs): + raise RuntimeError("subsequent setup failed") + + monkeypatch.setattr(orchestration, "LocalPostgres", Local) + monkeypatch.setattr(orchestration, "DatabaseOwner", Owner) + inputs = orchestration.FixedInputs( + role="candidate", + node=tmp_path / "node", + node_sha256="a" * 64, + install=tmp_path / "install", + tarball=tmp_path / "sdk.tgz", + tarball_sha256="b" * 64, + package_member_count=1, + package_member_manifest_sha256="c" * 64, + lock=tmp_path / "package-lock.json", + lock_sha256="d" * 64, + version="14.0.0-rc.45", + integrity="sha512-example", + adcp_version="3.2.0-rc.4", + inventory=inventory(), + cases=orchestration.validate_inventory(inventory()), + ) + args = SimpleNamespace( + output=tmp_path / "out", + execute=True, + external_pg_admin_dsn=None, + local_pg_bin=tmp_path / "bin", + local_pg_port=55_439, + service_port_base=55_500, + startup_timeout=1, + storyboard_timeout=1, + shutdown_timeout=1, + ) + with pytest.raises(RuntimeError, match="subsequent setup failed"): + orchestration.execute(inputs, args) + assert closed == [True] + + +def test_case_result_requires_exit_success_all_identities_and_no_skips() -> None: + case = orchestration.Case("story", 2, 2, "controlled") + row = { + "id": "story", + "steps": [ + {"id": "a", "title": "A", "task": "tool"}, + {"id": "b", "title": "B", "task": "tool"}, + ], + } + result = { + "storyboards_executed": ["story"], + "summary": {"steps_passed": 2}, + "tracks": [ + { + "scenarios": [ + { + "steps": [ + {"step": "A", "task": "tool", "passed": True}, + {"step": "B", "task": "tool", "passed": True}, + ] + } + ] + } + ], + } + assert orchestration.evaluate_case(case, row, result, 0)["fully_executed"] is True + result["tracks"][0]["scenarios"][0]["steps"][1]["skipped"] = True + assert orchestration.evaluate_case(case, row, result, 0)["fully_executed"] is False + assert orchestration.evaluate_case(case, row, result, 1)["fully_executed"] is False + + result["tracks"][0]["scenarios"][0]["steps"][1].pop("skipped") + result["tracks"][0]["scenarios"][0]["steps"].append( + {"step": "unexpected", "task": "tool", "passed": False} + ) + evaluated = orchestration.evaluate_case(case, row, result, 0) + assert evaluated["fully_executed"] is False + assert evaluated["observed_result_row_count"] == 3 + assert evaluated["unexpected_result_rows"] == [ + {"step": "unexpected", "task": "tool", "passed": False, "skipped": None} + ] + + +def test_plan_never_claims_matrix_or_release_acceptance(tmp_path: Path) -> None: + inputs = orchestration.FixedInputs( + role="candidate", + node=tmp_path / "node", + node_sha256="a" * 64, + install=tmp_path / "install", + tarball=tmp_path / "sdk.tgz", + tarball_sha256="c" * 64, + package_member_count=1, + package_member_manifest_sha256="d" * 64, + lock=tmp_path / "package-lock.json", + lock_sha256="b" * 64, + version="14.0.0-rc.45", + integrity="sha512-example", + adcp_version="3.2.0-rc.4", + inventory=inventory(), + cases=orchestration.validate_inventory(inventory()), + ) + args = SimpleNamespace( + execute=False, + external_pg_admin_dsn="postgresql://127.0.0.1/postgres", + startup_timeout=60, + storyboard_timeout=240, + shutdown_timeout=10, + ) + plan = orchestration._plan(inputs, args) + assert plan["acceptance"] is False + assert plan["blocking_acceptance"] is False + assert "probe_scheduler_dst_remains_mandatory" in " ".join(plan["limitations"]) + + +def test_execution_source_uses_no_pg_ctl_or_shell_process_wrapper() -> None: + source = MODULE_PATH.read_text(encoding="utf-8") + assert "pg_ctl" not in source + assert "shell=True" not in source + assert "start_new_session=True" in source + assert '"--execute"' in source + assert '"--sandbox"' in source + + +def test_archive_manifest_binds_every_installed_member(tmp_path: Path) -> None: + package = tmp_path / "install/node_modules/@adcp/sdk" + package.mkdir(parents=True) + (package / "package.json").write_bytes(b'{"name":"@adcp/sdk"}\n') + tarball = tmp_path / "sdk.tgz" + with tarfile.open(tarball, "w:gz") as stream: + payload = b'{"name":"@adcp/sdk"}\n' + member = tarfile.TarInfo("package/package.json") + member.size = len(payload) + stream.addfile(member, io.BytesIO(payload)) + pin = { + "tarball_sha256": orchestration.sha256(tarball), + "tarball_members": 1, + } + count, manifest = orchestration.validate_package_archive(tarball, tmp_path / "install", pin) + assert count == 1 + assert len(manifest) == 64 + (package / "package.json").write_bytes(b"changed") + with pytest.raises(orchestration.OrchestrationError, match="differs from registry archive"): + orchestration.validate_package_archive(tarball, tmp_path / "install", pin) + + +def test_previous_and_candidate_python_members_use_distinct_immutable_identities( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + fake_psycopg = types.ModuleType("psycopg") + fake_psycopg.sql = SimpleNamespace() + fake_conninfo = types.ModuleType("psycopg.conninfo") + fake_conninfo.conninfo_to_dict = lambda _value: {} + fake_conninfo.make_conninfo = lambda **_value: "" + monkeypatch.setitem(sys.modules, "psycopg", fake_psycopg) + monkeypatch.setitem(sys.modules, "psycopg.conninfo", fake_conninfo) + runner_path = ROOT / "scripts/ci/reporting_interop/run_foundation_matrix.py" + spec = importlib.util.spec_from_file_location("reporting_foundation_identity_test", runner_path) + assert spec is not None and spec.loader is not None + runner = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = runner + spec.loader.exec_module(runner) + + previous_wheel = tmp_path / "previous.whl" + with zipfile.ZipFile(previous_wheel, "w") as archive: + archive.writestr("adcp/__init__.py", b"previous") + pins = tmp_path / "pins.json" + pins.write_text( + json.dumps( + { + "python": { + "previous_released": { + "wheel_sha256": runner._sha256(previous_wheel), + } + } + } + ), + encoding="utf-8", + ) + monkeypatch.setattr(runner, "PINS", pins) + monkeypatch.setattr( + runner, + "_selected_python_build", + lambda: {"sdk_member_count": 9, "sdk_member_manifest_sha256": "c" * 64}, + ) + candidate = runner._expected_python_member_identity( + runner.PythonRuntime("wheel", tmp_path / "python"), + runner.PythonArtifact("wheel", tmp_path / "candidate.whl"), + ) + previous = runner._expected_python_member_identity( + runner.PythonRuntime("previous", tmp_path / "python"), + runner.PythonArtifact("previous", previous_wheel), + ) + assert candidate == { + "source": "selected_candidate_build_manifest", + "sdk_member_count": 9, + "sdk_member_manifest_sha256": "c" * 64, + } + assert previous["source"] == "exact_previous_wheel_members" + assert previous["sdk_member_count"] == 1 + assert previous["sdk_member_manifest_sha256"] != candidate["sdk_member_manifest_sha256"] + + +def test_matrix_binds_complete_typescript_install_to_its_role_archive( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + fake_psycopg = types.ModuleType("psycopg") + fake_psycopg.sql = SimpleNamespace() + fake_conninfo = types.ModuleType("psycopg.conninfo") + fake_conninfo.conninfo_to_dict = lambda _value: {} + fake_conninfo.make_conninfo = lambda **_value: "" + monkeypatch.setitem(sys.modules, "psycopg", fake_psycopg) + monkeypatch.setitem(sys.modules, "psycopg.conninfo", fake_conninfo) + runner_path = ROOT / "scripts/ci/reporting_interop/run_foundation_matrix.py" + spec = importlib.util.spec_from_file_location("reporting_matrix_archive_test", runner_path) + assert spec is not None and spec.loader is not None + runner = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = runner + spec.loader.exec_module(runner) + + install = tmp_path / "install" + package_root = install / "node_modules/@adcp/sdk" + (package_root / "dist").mkdir(parents=True) + members = { + "package/package.json": b'{"name":"@adcp/sdk","version":"14.0.0-test"}\n', + "package/dist/index.js": b"module.exports = {};\n", + } + (package_root / "package.json").write_bytes(members["package/package.json"]) + (package_root / "dist/index.js").write_bytes(members["package/dist/index.js"]) + integrity = "sha512-test" + lock = { + "packages": { + "node_modules/@adcp/sdk": { + "version": "14.0.0-test", + "integrity": integrity, + } + } + } + (install / "package-lock.json").write_text(json.dumps(lock), encoding="utf-8") + archive = tmp_path / "sdk.tgz" + with tarfile.open(archive, "w:gz") as stream: + for name, payload in members.items(): + info = tarfile.TarInfo(name) + info.size = len(payload) + stream.addfile(info, io.BytesIO(payload)) + pins = tmp_path / "pins.json" + pins.write_text( + json.dumps( + { + "typescript": { + "development_candidate_rc47": { + "version": "14.0.0-test", + "integrity": integrity, + "tarball_sha256": runner._sha256(archive), + "tarball_members": 2, + "package_lock_sha256": runner._sha256(install / "package-lock.json"), + "executable_harness_input": True, + } + } + } + ), + encoding="utf-8", + ) + monkeypatch.setattr(runner, "PINS", pins) + identity = runner._typescript_archive_identity( + runner.TypeScriptInstall("candidate", install), + runner.TypeScriptArchive("candidate", archive), + ) + assert identity["installed_member_count"] == 2 + + (package_root / "dist/index.js").write_text("changed\n", encoding="utf-8") + with pytest.raises(runner.HarnessError, match="installed SDK differs"): + runner._typescript_archive_identity( + runner.TypeScriptInstall("candidate", install), + runner.TypeScriptArchive("candidate", archive), + ) + + +def test_storyboard_role_map_uses_the_declared_lead_pin( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + pins = tmp_path / "pins.json" + pins.write_text( + json.dumps({"typescript": {"newer_rc_lead": {"version": "lead"}}}), + encoding="utf-8", + ) + monkeypatch.setattr(orchestration, "PINS", pins) + assert orchestration._typescript_pin("lead") == {"version": "lead"} + + +def test_storyboard_candidate_map_refuses_historical_rc45_substitution( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + pins = tmp_path / "pins.json" + pins.write_text( + json.dumps( + { + "typescript": { + "development_candidate_rc47": { + "version": "14.0.0-rc.47", + "protocol": "3.2.0-rc.6", + "executable_harness_input": False, + }, + "historical_candidate_rc45": { + "version": "14.0.0-rc.45", + "protocol": "3.2.0-rc.4", + }, + } + } + ), + encoding="utf-8", + ) + monkeypatch.setattr(orchestration, "PINS", pins) + assert orchestration._typescript_pin("candidate")["version"] == "14.0.0-rc.47" + assert orchestration._typescript_pin("historical_candidate")["version"] == "14.0.0-rc.45" + + +def test_foundation_lead_core_control_retains_real_rc44_protocol_in_result( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + fake_psycopg = types.ModuleType("psycopg") + fake_psycopg.sql = SimpleNamespace() + fake_conninfo = types.ModuleType("psycopg.conninfo") + fake_conninfo.conninfo_to_dict = lambda _value: {} + fake_conninfo.make_conninfo = lambda **_value: "" + monkeypatch.setitem(sys.modules, "psycopg", fake_psycopg) + monkeypatch.setitem(sys.modules, "psycopg.conninfo", fake_conninfo) + runner_path = ROOT / "scripts/ci/reporting_interop/run_foundation_matrix.py" + spec = importlib.util.spec_from_file_location("reporting_matrix_lead_result", runner_path) + assert spec is not None and spec.loader is not None + runner = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = runner + spec.loader.exec_module(runner) + + install = runner.TypeScriptInstall("lead", ROOT / "scripts/ci/reporting_interop/npm/rc44") + pin = runner._typescript_pin(install) + assert pin["version"] == "14.0.0-rc.44" + assert pin["protocol"] == "3.2.0-rc.4" + + output = tmp_path / "result" + output.mkdir() + calls = iter([{"definitive": True}, {"status": "passed"}]) + monkeypatch.setattr( + runner, + "_create_database", + lambda _admin, name: {"name": name, "cluster_identity": "fake-cluster"}, + ) + monkeypatch.setattr(runner, "_node_database_url", lambda *_args: "postgresql://fake/db") + monkeypatch.setattr(runner, "_free_port", lambda: 55_555) + monkeypatch.setattr( + runner.subprocess, + "Popen", + lambda *_args, **_kwargs: SimpleNamespace(pid=4242), + ) + + def fake_ready(_process, ready_file, **_kwargs): + ready_file.write_text("{}\n", encoding="utf-8") + return { + "pid": 4242, + "process_start_token": "123", + "startup_proof_sha256": "f" * 64, + "seller_package": { + "integrity": pin["integrity"], + "language": "typescript", + "name": "@adcp/sdk", + "package_role": "lead", + "protocol": pin["protocol"], + "version": pin["version"], + }, + } + + monkeypatch.setattr(runner, "_wait_core_seller_ready", fake_ready) + monkeypatch.setattr(runner, "_run_json", lambda *_args, **_kwargs: next(calls)) + monkeypatch.setattr(runner, "_stop", lambda _process: None) + monkeypatch.setattr( + runner, + "_runtime_identity", + lambda *_args: {"route": "wheel", "identity": "fake-candidate"}, + ) + + rows = runner._run_ts_core_control( + runner.PythonRuntime("wheel", tmp_path / "python"), + runner.PythonArtifact("wheel", tmp_path / "candidate.whl"), + node=tmp_path / "node", + install=install, + admin_url="postgresql://fake/postgres", + output=output, + database="lead-control", + ) + + assert len(rows) == 2 + assert {row["observed_protocol"] for row in rows} == {"3.2.0-rc.4"} + assert all(row["required_cell_credit"] is False for row in rows) + assert rows[1]["id"].startswith("supplemental_lead_shared__") + assert rows[1]["control_classification"] == "supplemental_lead_shared_seller_control" + assert rows[1]["acceptance"] is False + + +def test_core_seller_credentials_fail_closed_on_low_entropy_or_reuse( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + fake_psycopg = types.ModuleType("psycopg") + fake_psycopg.sql = SimpleNamespace() + fake_conninfo = types.ModuleType("psycopg.conninfo") + fake_conninfo.conninfo_to_dict = lambda _value: {} + fake_conninfo.make_conninfo = lambda **_value: "" + monkeypatch.setitem(sys.modules, "psycopg", fake_psycopg) + monkeypatch.setitem(sys.modules, "psycopg.conninfo", fake_conninfo) + runner_path = ROOT / "scripts/ci/reporting_interop/run_foundation_matrix.py" + spec = importlib.util.spec_from_file_location("reporting_matrix_secret_test", runner_path) + assert spec is not None and spec.loader is not None + runner = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = runner + spec.loader.exec_module(runner) + + monkeypatch.setattr(runner.secrets, "token_urlsafe", lambda _size: "fixed") + monkeypatch.setattr(runner.secrets, "token_hex", lambda _size: "fixed") + with pytest.raises(runner.HarnessError, match="low-entropy or repeated"): + runner._new_core_seller_credentials() + + values = iter(["a" * 43, "b" * 43]) + monkeypatch.setattr(runner.secrets, "token_urlsafe", lambda _size: next(values)) + monkeypatch.setattr(runner.secrets, "token_hex", lambda _size: "c" * 64) + credentials = runner._new_core_seller_credentials() + assert credentials.token_a != credentials.token_b + command = runner._ts_server_command( + tmp_path / "node", + runner.TypeScriptInstall("lead", ROOT / "scripts/ci/reporting_interop/npm/rc44"), + port=55_555, + database_url="postgresql://fixture/db", + ready_file=ROOT / "seller.ready.unit.json", + ) + assert "interop-token-a" not in command + assert credentials.token_a not in command and credentials.token_b not in command + assert credentials.startup_proof not in command + assert "--auth-token-a" not in command and "--auth-token-b" not in command + assert "--startup-proof" not in command + environment = runner._ts_server_environment(credentials) + assert environment[runner.TS_CORE_PRIVATE_ENV["token_a"]] == credentials.token_a + assert environment[runner.TS_CORE_PRIVATE_ENV["token_b"]] == credentials.token_b + assert environment[runner.TS_CORE_PRIVATE_ENV["startup_proof"]] == credentials.startup_proof + assert str(tmp_path / "node") == command[0] + launches: list[tuple[list[str], dict[str, object]]] = [] + + def capture_launch(argv, **kwargs): + launches.append((argv, kwargs)) + return SimpleNamespace(pid=4242) + + monkeypatch.setattr(runner.subprocess, "Popen", capture_launch) + monkeypatch.setattr( + runner, + "_typescript_pin", + lambda install: { + "package": "@adcp/sdk", + "version": f"14.0.0-{install.role}", + "integrity": f"sha512-{install.role}", + "protocol": "3.2.0-rc.6", + }, + ) + for role in ("candidate", "previous", "lead"): + runner._start_ts_core_server( + tmp_path / "node", + runner.TypeScriptInstall(role, ROOT / "scripts/ci/reporting_interop/npm/rc44"), + port=55_555, + database_url="postgresql://fixture/db", + credentials=credentials, + ready_file=tmp_path / f"{role}.ready.json", + cwd=tmp_path, + stdout=io.BytesIO(), + stderr=io.BytesIO(), + ) + assert len(launches) == 3 + for argv, kwargs in launches: + assert credentials.token_a not in argv + assert credentials.token_b not in argv + assert credentials.startup_proof not in argv + child_environment = kwargs["env"] + assert child_environment[runner.TS_CORE_PRIVATE_ENV["token_a"]] == credentials.token_a + assert child_environment[runner.TS_CORE_PRIVATE_ENV["token_b"]] == credentials.token_b + assert ( + child_environment[runner.TS_CORE_PRIVATE_ENV["startup_proof"]] + == credentials.startup_proof + ) + + +def test_foundation_handoffs_use_narrow_environments_for_every_scoped_route( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + fake_psycopg = types.ModuleType("psycopg") + fake_psycopg.sql = SimpleNamespace() + fake_conninfo = types.ModuleType("psycopg.conninfo") + fake_conninfo.conninfo_to_dict = lambda _value: {} + fake_conninfo.make_conninfo = lambda **_value: "" + monkeypatch.setitem(sys.modules, "psycopg", fake_psycopg) + monkeypatch.setitem(sys.modules, "psycopg.conninfo", fake_conninfo) + runner_path = ROOT / "scripts/ci/reporting_interop/run_foundation_matrix.py" + spec = importlib.util.spec_from_file_location("reporting_matrix_environment_test", runner_path) + assert spec is not None and spec.loader is not None + runner = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = runner + spec.loader.exec_module(runner) + + parent = { + "PATH": "/runtime/bin", + "LANG": "C.UTF-8", + "TZ": "Etc/UTC", + "AMBIENT_TOKEN": "ambient-token", + "AWS_SECRET_ACCESS_KEY": "ambient-cloud-secret", + "UNRELATED_PASSWORD": "ambient-password", + "DATABASE_URL": "postgresql://ambient/unsafe", + "ADCP_AUTH_TOKEN": "stale-cli-token", + runner.TS_CORE_PRIVATE_ENV["token_a"]: "stale-a", + runner.TS_CORE_PRIVATE_ENV["token_b"]: "stale-b", + runner.TS_CORE_PRIVATE_ENV["startup_proof"]: "stale-proof", + } + original_parent = dict(parent) + monkeypatch.setattr(runner.os, "environ", parent) + monkeypatch.setattr( + runner, + "_typescript_pin", + lambda install: { + "package": "@adcp/sdk", + "version": f"14.0.0-{install.role}", + "integrity": f"sha512-{install.role}", + "protocol": "3.2.0-rc.4", + }, + ) + monkeypatch.setattr( + runner, + "_create_database", + lambda _admin, name: {"name": name, "cluster_identity": "fixture-cluster"}, + ) + monkeypatch.setattr(runner, "_node_database_url", lambda *_args: "postgresql://owned/db") + monkeypatch.setattr(runner, "_free_port", lambda: 55_555) + monkeypatch.setattr(runner, "_wait_port", lambda *_args, **_kwargs: None) + monkeypatch.setattr(runner, "_stop", lambda _process: None) + monkeypatch.setattr(runner, "_runtime_identity", lambda *_args: {"route": "fixture"}) + monkeypatch.setattr( + runner, + "_sha256", + lambda path: "previous-wheel-hash" if path.name == "previous.whl" else "fixture-hash", + ) + + pins = tmp_path / "pins.json" + pins.write_text( + json.dumps({"python": {"previous_released": {"wheel_sha256": "previous-wheel-hash"}}}), + encoding="utf-8", + ) + monkeypatch.setattr(runner, "PINS", pins) + + buyer_handoffs: list[tuple[list[str], dict[str, str]]] = [] + seller_handoffs: list[tuple[list[str], dict[str, str]]] = [] + + def record_popen(command, **kwargs): + if "env" in kwargs: + seller_handoffs.append((list(command), dict(kwargs["env"]))) + return SimpleNamespace(pid=4242) + + def record_direct_buyer(command, **kwargs): + buyer_handoffs.append((list(command), dict(kwargs["env"]))) + return SimpleNamespace( + returncode=0, + stdout=json.dumps({"definitive": True, "submitted_receipts": 0}), + stderr="", + ) + + def record_json_buyer(command, *, cwd, env): + del cwd + buyer_handoffs.append((list(command), dict(env))) + if "--expect-unsupported" in command: + return { + "status": "actionable_unsupported", + "mutation_attempted": False, + "phase": "client_construction", + } + return {"definitive": True, "submitted_receipts": 0, "status": "passed"} + + def record_ready(process, ready_file, *, install, credentials, **_kwargs): + pin = runner._typescript_pin(install) + ready_file.write_text("{}\n", encoding="utf-8") + return { + "pid": process.pid, + "process_start_token": "123", + "startup_proof_sha256": hashlib.sha256(credentials.startup_proof.encode()).hexdigest(), + "seller_package": { + "name": "@adcp/sdk", + "language": "typescript", + "package_role": install.role, + "version": pin["version"], + "integrity": pin["integrity"], + "protocol": pin["protocol"], + }, + } + + monkeypatch.setattr(runner.subprocess, "Popen", record_popen) + monkeypatch.setattr(runner.subprocess, "run", record_direct_buyer) + monkeypatch.setattr(runner, "_run_json", record_json_buyer) + monkeypatch.setattr(runner, "_wait_core_seller_ready", record_ready) + + runtime = runner.PythonRuntime("wheel", tmp_path / "python") + artifact = runner.PythonArtifact("wheel", tmp_path / "candidate.whl") + node = tmp_path / "node" + candidate = runner.TypeScriptInstall("candidate", tmp_path / "candidate-ts") + previous = runner.TypeScriptInstall("previous", tmp_path / "previous-ts") + for install in (candidate, previous): + install.root.mkdir() + + route_outputs = [tmp_path / f"route-{index}" for index in range(4)] + for output in route_outputs: + output.mkdir() + runner._run_python_control( + runtime, + artifact, + admin_url="postgresql://owned/postgres", + output=route_outputs[0], + database="python-control", + node=node, + candidate_ts=candidate, + ) + runner._run_ts_core_control( + runtime, + artifact, + node=node, + install=candidate, + admin_url="postgresql://owned/postgres", + output=route_outputs[1], + database="typescript-control", + ) + runner._run_candidate_python_previous_ts( + runtime, + artifact, + node=node, + install=previous, + admin_url="postgresql://owned/postgres", + output=route_outputs[2], + database="positive-skew", + ) + runner._run_previous_python_candidate_ts( + runner.PreviousPythonInput(tmp_path / "previous-python", tmp_path / "previous.whl"), + node=node, + install=candidate, + admin_url="postgresql://owned/postgres", + output=route_outputs[3], + database="negative-skew", + ) + + assert len(buyer_handoffs) == 6 + assert len(seller_handoffs) == 3 + allowed_runtime = {"PATH": "/runtime/bin", "LANG": "C.UTF-8", "TZ": "Etc/UTC"} + excluded = { + "AMBIENT_TOKEN", + "AWS_SECRET_ACCESS_KEY", + "UNRELATED_PASSWORD", + "DATABASE_URL", + "ADCP_AUTH_TOKEN", + *runner.TS_CORE_PRIVATE_ENV.values(), + } + for command, environment in buyer_handoffs: + assert {key: environment[key] for key in allowed_runtime} == allowed_runtime + assert excluded.isdisjoint(environment) + assert set(environment) == {*allowed_runtime, "ADCP_INTEROP_BUYER_TOKEN"} + assert environment["ADCP_INTEROP_BUYER_TOKEN"] not in command + for command, environment in seller_handoffs: + assert {key: environment[key] for key in allowed_runtime} == allowed_runtime + assert excluded.isdisjoint(set(environment) - set(runner.TS_CORE_PRIVATE_ENV.values())) + assert set(environment) == {*allowed_runtime, *runner.TS_CORE_PRIVATE_ENV.values()} + for key in runner.TS_CORE_PRIVATE_ENV.values(): + assert environment[key] not in command + + assert [env["ADCP_INTEROP_BUYER_TOKEN"] for _, env in buyer_handoffs[:2]] == [ + "interop-token-a", + "interop-token-a", + ] + assert [env["ADCP_INTEROP_BUYER_TOKEN"] for _, env in buyer_handoffs[2:4]] == [ + seller_handoffs[0][1][runner.TS_CORE_PRIVATE_ENV["token_a"]], + seller_handoffs[0][1][runner.TS_CORE_PRIVATE_ENV["token_a"]], + ] + assert ( + buyer_handoffs[4][1]["ADCP_INTEROP_BUYER_TOKEN"] + == seller_handoffs[1][1][runner.TS_CORE_PRIVATE_ENV["token_a"]] + ) + assert ( + buyer_handoffs[5][1]["ADCP_INTEROP_BUYER_TOKEN"] + == seller_handoffs[2][1][runner.TS_CORE_PRIVATE_ENV["token_a"]] + ) + assert parent == original_parent + + +def test_core_seller_readiness_rejects_a_misrouted_seller( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + fake_psycopg = types.ModuleType("psycopg") + fake_psycopg.sql = SimpleNamespace() + fake_conninfo = types.ModuleType("psycopg.conninfo") + fake_conninfo.conninfo_to_dict = lambda _value: {} + fake_conninfo.make_conninfo = lambda **_value: "" + monkeypatch.setitem(sys.modules, "psycopg", fake_psycopg) + monkeypatch.setitem(sys.modules, "psycopg.conninfo", fake_conninfo) + runner_path = ROOT / "scripts/ci/reporting_interop/run_foundation_matrix.py" + spec = importlib.util.spec_from_file_location("reporting_matrix_ready_test", runner_path) + assert spec is not None and spec.loader is not None + runner = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = runner + spec.loader.exec_module(runner) + + install = runner.TypeScriptInstall("lead", ROOT / "scripts/ci/reporting_interop/npm/rc44") + pin = runner._typescript_pin(install) + credentials = runner.CoreSellerCredentials("a" * 43, "b" * 43, "c" * 64) + node = tmp_path / "node" + node.write_text("fixture", encoding="utf-8") + ready_file = tmp_path / "ready.json" + process = SimpleNamespace(pid=4242, poll=lambda: None) + monkeypatch.setattr(runner, "_linux_process_start_token", lambda _pid: "123") + ready = { + "kind": "reporting_interop_ts_core_server_ready", + "pid": 4242, + "port": 55_555, + "process_start_token": "123", + "startup_proof_sha256": hashlib.sha256(credentials.startup_proof.encode()).hexdigest(), + "auth_binding_sha256": { + "interop-account-a": hashlib.sha256(credentials.token_a.encode()).hexdigest(), + "interop-account-b": hashlib.sha256(credentials.token_b.encode()).hexdigest(), + }, + "node": {"executable": str(node.resolve()), "version": "v22.12.0"}, + "seller_package": { + "name": "@adcp/sdk", + "language": "typescript", + "package_role": "candidate", + "version": pin["version"], + "integrity": pin["integrity"], + "protocol": pin["protocol"], + }, + } + ready_file.write_text(json.dumps(ready), encoding="utf-8") + with pytest.raises(runner.HarnessError, match="selected seller"): + runner._wait_core_seller_ready( + process, + ready_file, + node=node, + install=install, + credentials=credentials, + port=55_555, + timeout=0.1, + ) + + ready["seller_package"]["package_role"] = "lead" + ready_file.write_text(json.dumps(ready), encoding="utf-8") + observed = runner._wait_core_seller_ready( + process, + ready_file, + node=node, + install=install, + credentials=credentials, + port=55_555, + timeout=0.1, + ) + assert observed["seller_package"]["package_role"] == "lead" + + +def test_foundation_accounting_rejects_id_only_rc4_and_negative_skew_credit( + monkeypatch: pytest.MonkeyPatch, +) -> None: + fake_psycopg = types.ModuleType("psycopg") + fake_psycopg.sql = SimpleNamespace() + fake_conninfo = types.ModuleType("psycopg.conninfo") + fake_conninfo.conninfo_to_dict = lambda _value: {} + fake_conninfo.make_conninfo = lambda **_value: "" + monkeypatch.setitem(sys.modules, "psycopg", fake_psycopg) + monkeypatch.setitem(sys.modules, "psycopg.conninfo", fake_conninfo) + runner_path = ROOT / "scripts/ci/reporting_interop/run_foundation_matrix.py" + spec = importlib.util.spec_from_file_location("reporting_matrix_accounting_test", runner_path) + assert spec is not None and spec.loader is not None + runner = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = runner + spec.loader.exec_module(runner) + + applicability = { + "cells": [ + { + "id": "q1", + "contract_id": "Q1", + "family": "candidate_quadrant", + "blocking": True, + "protocol_contract": { + "required": "3.2.0-rc.6", + "alignment": "exact", + "selected_artifacts": True, + "artifact_identity_sha256": "a" * 64, + }, + }, + { + "id": "s1", + "contract_id": "S1", + "family": "supported_skew", + "blocking": True, + "positive_requirement": { + "status": "selected_supported_pair", + "blocking": True, + "artifact_identity_sha256": "b" * 64, + }, + }, + { + "id": "latest", + "family": "latest_canary", + "blocking": False, + }, + ] + } + base = { + "execution_attempted": True, + "required_cell_credit": True, + "artifact_identity_sha256": "a" * 64, + "seller_process_identity": { + "pid": 101, + "start_token": "10", + "startup_proof_sha256": "e" * 64, + }, + "seller_artifact_identity": { + "language": "typescript", + "package_role": "candidate", + "runtime_identity_sha256": "c" * 64, + "artifact_identity_sha256": "d" * 64, + }, + "database_identity": {"name": "database-1", "cluster_identity": "cluster-1"}, + "cell_complete": True, + } + rows = runner._execution_accounting( + applicability, + [ + { + **base, + "id": "q1", + "target_contract_id": "Q1", + "observed_protocol": "3.2.0-rc.4", + "polarity": "positive_semantic", + }, + { + **base, + "id": "negative_skew_refusal__s1", + "target_cell_id": "s1", + "target_contract_id": "S1", + "artifact_identity_sha256": "b" * 64, + "polarity": "negative_refusal", + }, + ], + ) + by_id = {row["id"]: row for row in rows} + assert by_id["q1"]["executed_with_required_credit"] is False + assert by_id["s1"]["executed_with_required_credit"] is False + assert by_id["latest"]["executed_with_required_credit"] is False + assert ( + "observed_protocol_does_not_match_required" in by_id["q1"]["observed_controls"][0]["errors"] + ) + assert ( + "negative_or_partial_skew_cannot_satisfy_positive_requirement" + in by_id["s1"]["observed_controls"][0]["errors"] + ) + + +def test_foundation_accounting_requires_fresh_identity_and_positive_semantics( + monkeypatch: pytest.MonkeyPatch, +) -> None: + fake_psycopg = types.ModuleType("psycopg") + fake_psycopg.sql = SimpleNamespace() + fake_conninfo = types.ModuleType("psycopg.conninfo") + fake_conninfo.conninfo_to_dict = lambda _value: {} + fake_conninfo.make_conninfo = lambda **_value: "" + monkeypatch.setitem(sys.modules, "psycopg", fake_psycopg) + monkeypatch.setitem(sys.modules, "psycopg.conninfo", fake_conninfo) + runner_path = ROOT / "scripts/ci/reporting_interop/run_foundation_matrix.py" + spec = importlib.util.spec_from_file_location( + "reporting_matrix_positive_accounting", runner_path + ) + assert spec is not None and spec.loader is not None + runner = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = runner + spec.loader.exec_module(runner) + + applicability = { + "cells": [ + { + "id": "q1", + "contract_id": "Q1", + "family": "candidate_quadrant", + "blocking": True, + "server": { + "language": "typescript", + "package_role": "candidate", + "artifact_identity_sha256": "d" * 64, + "runtime_identity_sha256": "c" * 64, + }, + "protocol_contract": { + "required": "3.2.0-rc.6", + "alignment": "exact", + "selected_artifacts": True, + "artifact_identity_sha256": "a" * 64, + }, + } + ] + } + observed = { + "id": "q1", + "target_contract_id": "Q1", + "execution_attempted": True, + "required_cell_credit": True, + "artifact_identity_sha256": "a" * 64, + "seller_process_identity": { + "pid": 101, + "start_token": "10", + "startup_proof_sha256": "e" * 64, + }, + "seller_artifact_identity": { + "language": "typescript", + "package_role": "candidate", + "runtime_identity_sha256": "c" * 64, + "artifact_identity_sha256": "d" * 64, + }, + "database_identity": {"name": "database-1", "cluster_identity": "cluster-1"}, + "observed_protocol": "3.2.0-rc.6", + "polarity": "positive_semantic", + "cell_complete": True, + } + [row] = runner._execution_accounting(applicability, [observed]) + assert row["executed_with_required_credit"] is True + assert row["completed_with_required_credit"] is True + + misrouted = { + **observed, + "seller_artifact_identity": { + **observed["seller_artifact_identity"], + "artifact_identity_sha256": "e" * 64, + }, + # A legitimate cross-language buyer identity must not be compared to + # the seller archive identity. + "buyer_artifact_identity_sha256": "f" * 64, + } + [row] = runner._execution_accounting(applicability, [misrouted]) + assert row["executed_with_required_credit"] is False + assert ( + "seller_archive_identity_does_not_match_cell_server" + in row["observed_controls"][0]["errors"] + ) + + second_contract = { + **applicability["cells"][0], + "id": "q2", + "contract_id": "Q2", + } + second_observed = { + **observed, + "id": "q2", + "target_contract_id": "Q2", + } + duplicate_rows = runner._execution_accounting( + {"cells": [applicability["cells"][0], second_contract]}, + [observed, second_observed], + ) + assert all(row["executed_with_required_credit"] is False for row in duplicate_rows) + assert all( + "seller_process_identity_not_unique" in row["observed_controls"][0]["errors"] + and "database_identity_not_unique" in row["observed_controls"][0]["errors"] + for row in duplicate_rows + ) + + del observed["database_identity"] + [row] = runner._execution_accounting(applicability, [observed]) + assert row["executed_with_required_credit"] is False + assert "database_identity_missing" in row["observed_controls"][0]["errors"] + + +def test_controlled_server_routes_only_declared_storyboard_accounts() -> None: + server = (ROOT / "scripts/ci/reporting_interop/ts_controlled_reporting_server.cjs").read_text( + encoding="utf-8" + ) + fixture = (ROOT / "scripts/ci/reporting_interop/ts_controlled_reporting_fixture.cjs").read_text( + encoding="utf-8" + ) + + assert "function createAccountRouter(storyboardId)" in server + assert "request?.account?.operator !== STORYBOARD_OPERATOR" in server + assert "request?.account?.sandbox !== true" in server + assert "consumerPrepareCount >= consumerAccounts.length" in server + assert "corePrepareCount++ === 0" in server + assert "--storyboard-id" in server + assert "reporting_consumer_content_lab" in fixture From fd7d5e6fb113d53f3920b6801cf5c25f6bb98d94 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 26 Sep 2026 16:28:25 +0000 Subject: [PATCH 2/7] fix(reporting): address interop code review --- .../run_foundation_matrix.py | 3 +-- .../signing/run-signing-vectors.cjs | 2 +- .../storyboard_orchestration.py | 24 ++++++++----------- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/scripts/ci/reporting_interop/run_foundation_matrix.py b/scripts/ci/reporting_interop/run_foundation_matrix.py index 10fe70706..919286b78 100644 --- a/scripts/ci/reporting_interop/run_foundation_matrix.py +++ b/scripts/ci/reporting_interop/run_foundation_matrix.py @@ -993,8 +993,7 @@ def _python_webhook_vector_dir(runtime: PythonRuntime) -> Path: str(runtime.executable), "-I", "-c", - "import adcp,pathlib; print(pathlib.Path(adcp.__file__).parent / " - "'_compliance/3.2.0-rc.4/test-vectors/webhook-signing')", + "import adcp,pathlib; print(pathlib.Path(adcp.__file__).parent / '_compliance/3.2.0-rc.4/test-vectors/webhook-signing')", ], check=True, capture_output=True, diff --git a/scripts/ci/reporting_interop/signing/run-signing-vectors.cjs b/scripts/ci/reporting_interop/signing/run-signing-vectors.cjs index 7572480d0..d6d42eb66 100644 --- a/scripts/ci/reporting_interop/signing/run-signing-vectors.cjs +++ b/scripts/ci/reporting_interop/signing/run-signing-vectors.cjs @@ -7,7 +7,7 @@ const crypto = require('node:crypto'); const { spawn } = require('node:child_process'); const fs = require('node:fs'); const { mintEphemeralEd25519Key } = require('@adcp/sdk/signing/testing'); -const { signRequest, computeContentDigest, requestSigningEncodingForVersion } = require('@adcp/sdk/signing/client'); +const { signRequest, requestSigningEncodingForVersion } = require('@adcp/sdk/signing/client'); const { verifyWebhookRequest } = require('@adcp/sdk/webhooks'); const { CLOCK_SKEW_TOLERANCE_SECONDS, MAX_SIGNATURE_WINDOW_SECONDS, ALLOWED_ALGS, MANDATORY_COMPONENTS, WEBHOOK_MANDATORY_COMPONENTS } = require('@adcp/sdk/signing/server'); diff --git a/scripts/ci/reporting_interop/storyboard_orchestration.py b/scripts/ci/reporting_interop/storyboard_orchestration.py index bd6c65f16..b075ca840 100644 --- a/scripts/ci/reporting_interop/storyboard_orchestration.py +++ b/scripts/ci/reporting_interop/storyboard_orchestration.py @@ -453,9 +453,7 @@ def __init__( self._identity_reader = identity_reader self._member_reader = member_reader self._pidfd_open = pidfd_open or os.pidfd_open - self._pidfd_signal = pidfd_signal or ( - lambda descriptor, sig: signal.pidfd_send_signal(descriptor, sig) - ) + self._pidfd_signal = pidfd_signal or signal.pidfd_send_signal self._pidfd_close = pidfd_close self.pidfd = pidfd self.death_proven = process.poll() is not None @@ -487,9 +485,7 @@ def spawn( assert_process_ownership_supported() open_pidfd = pidfd_open or os.pidfd_open - send_pidfd = pidfd_signal or ( - lambda descriptor, sig: signal.pidfd_send_signal(descriptor, sig) - ) + send_pidfd = pidfd_signal or signal.pidfd_send_signal gate_read, gate_write = os.pipe() inherited = tuple(kwargs.pop("pass_fds", ())) launcher = ( @@ -508,7 +504,7 @@ def spawn( ] try: process = popen(wrapped, pass_fds=(*inherited, gate_read), **kwargs) - except BaseException: + except (Exception, KeyboardInterrupt, SystemExit): os.close(gate_read) os.close(gate_write) raise @@ -1107,13 +1103,13 @@ def _run_owned_to_files( f"{stdout_path} and {stderr_path}" ) primary.__cause__ = error - except BaseException as error: # preserve interrupts while guaranteeing owned cleanup + except (Exception, KeyboardInterrupt, SystemExit) as error: primary = error finally: if owned is not None: try: owned.stop(shutdown_timeout) - except BaseException as error: + except (Exception, KeyboardInterrupt, SystemExit) as error: cleanup = error if primary is not None and cleanup is not None: raise OrchestrationError( @@ -1233,13 +1229,13 @@ def _run_case( {"node_path": str(inputs.node), "node_sha256": inputs.node_sha256} ), } - except BaseException as error: + except (Exception, KeyboardInterrupt, SystemExit) as error: primary = error finally: if process is not None: try: process.stop(shutdown_timeout) - except BaseException as error: + except (Exception, KeyboardInterrupt, SystemExit) as error: cleanup = error if primary is not None and cleanup is not None: raise OrchestrationError( @@ -1368,18 +1364,18 @@ def run(index: int, case: Case) -> dict[str, Any]: admin_dsn = local.start() owner = DatabaseOwner(admin_dsn, secrets.token_hex(6), args.startup_timeout) rows, errors = execute_case_sequence(inputs.cases, run, lambda: None) - except BaseException as error: + except (Exception, KeyboardInterrupt, SystemExit) as error: primary = error finally: if owner is not None: try: owner.close() - except BaseException as error: + except (Exception, KeyboardInterrupt, SystemExit) as error: cleanup_errors.append(f"database cleanup: {error}") if local is not None: try: local.close() - except BaseException as error: + except (Exception, KeyboardInterrupt, SystemExit) as error: cleanup_errors.append(f"PostgreSQL cleanup: {error}") if primary is not None and cleanup_errors: raise OrchestrationError( From e9f734c4bf56f1da6d829a2631631c283f3bc196 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 26 Sep 2026 17:18:34 +0000 Subject: [PATCH 3/7] docs(reporting): explain cleanup polling guards --- scripts/ci/reporting_interop/storyboard_orchestration.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/ci/reporting_interop/storyboard_orchestration.py b/scripts/ci/reporting_interop/storyboard_orchestration.py index b075ca840..28c27ed0f 100644 --- a/scripts/ci/reporting_interop/storyboard_orchestration.py +++ b/scripts/ci/reporting_interop/storyboard_orchestration.py @@ -539,6 +539,7 @@ def spawn( try: os.close(gate_write) except OSError: + # Best-effort descriptor cleanup must not replace the primary spawn error. pass try: if process.poll() is None: @@ -608,6 +609,7 @@ def _wait_until_settled(self, timeout: float) -> bool: try: self.process.wait(timeout=0) except subprocess.TimeoutExpired: + # A live child is expected during this zero-timeout polling probe. pass if self.process.poll() is not None and not self._live_members(): return True From c5b5826d6caea2eb267ead5c288c4068438ed0cf Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 26 Sep 2026 19:12:41 +0000 Subject: [PATCH 4/7] test(reporting): require helper probe completion --- tests/test_reporting_interop_corpus.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_reporting_interop_corpus.py b/tests/test_reporting_interop_corpus.py index f3c1bd337..776119d90 100644 --- a/tests/test_reporting_interop_corpus.py +++ b/tests/test_reporting_interop_corpus.py @@ -881,9 +881,18 @@ class FakePool {{ }}), /first failure/); assert.equal(boundary.signal.aborted, true); boundary.dispose(); + process.stdout.write('ts-version-abort-helpers-complete\\n'); }})().catch(error => {{ console.error(error); process.exitCode = 1; }}); """ - subprocess.run(["node", "-e", script], check=True, cwd=ROOT) + completed = subprocess.run( + ["node", "-e", script], + check=True, + cwd=ROOT, + capture_output=True, + text=True, + timeout=30, + ) + assert completed.stdout.splitlines() == ["ts-version-abort-helpers-complete"] assert "urllib.request.urlopen" in receiver_server assert '"normative_019_match"' in receiver_client assert '"blocking_acceptance"' in receiver_client From 0bdf79c1cd8cc4c5409482b1e162748fd8b1bfdd Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 26 Sep 2026 23:23:27 +0000 Subject: [PATCH 5/7] fix(reporting): bound interop harness cleanup --- .../run_foundation_matrix.py | 237 ++++++++++++---- scripts/ci/reporting_interop_matrix.py | 131 +++++++-- tests/test_reporting_interop_matrix.py | 42 +++ ...test_reporting_storyboard_orchestration.py | 253 +++++++++++++++++- 4 files changed, 584 insertions(+), 79 deletions(-) diff --git a/scripts/ci/reporting_interop/run_foundation_matrix.py b/scripts/ci/reporting_interop/run_foundation_matrix.py index 919286b78..dc5ad07ae 100644 --- a/scripts/ci/reporting_interop/run_foundation_matrix.py +++ b/scripts/ci/reporting_interop/run_foundation_matrix.py @@ -68,6 +68,9 @@ NARROW_RUNTIME_ENV_KEYS = frozenset( {"PATH", "LANG", "LC_ALL", "TZ", "TMPDIR", "TEMP", "TMP", "SYSTEMROOT", "WINDIR"} ) +DATABASE_OPERATION_TIMEOUT_SECONDS = 5 +STORYBOARD_EXECUTION_TIMEOUT_SECONDS = 1_500 +STORYBOARD_SHUTDOWN_TIMEOUT_SECONDS = 30 class HarnessError(RuntimeError): @@ -446,29 +449,72 @@ def _node_database_url(admin_url: str, name: str) -> str: return f"postgresql://{credentials}{host}:{port}/{quote(name, safe='')}{suffix}" -def _create_database(admin_url: str, name: str) -> dict[str, str]: - with psycopg.connect(admin_url, autocommit=True) as connection: +def _database_connect(dsn: str, *, autocommit: bool): + milliseconds = DATABASE_OPERATION_TIMEOUT_SECONDS * 1_000 + return psycopg.connect( + dsn, + autocommit=autocommit, + connect_timeout=DATABASE_OPERATION_TIMEOUT_SECONDS, + options=f"-c statement_timeout={milliseconds} -c lock_timeout={milliseconds}", + ) + + +def _create_database(admin_url: str, name: str) -> dict[str, Any]: + with _database_connect(admin_url, autocommit=True) as connection: statement = sql.SQL( "CREATE DATABASE {} TEMPLATE template0 ENCODING 'UTF8' LC_COLLATE 'C' LC_CTYPE 'C'" ).format(sql.Identifier(name)) connection.execute(statement) url = _database_url(admin_url, name) - with psycopg.connect(url) as connection: + with _database_connect(url, autocommit=False) as connection: row = connection.execute( - "SELECT current_database(), pg_encoding_to_char(encoding), datcollate " + "SELECT current_database(), pg_encoding_to_char(encoding), datcollate, " + "current_setting('data_directory'), pg_postmaster_start_time()::text, " + "(SELECT system_identifier::text FROM pg_control_system()) " "FROM pg_database WHERE datname=current_database()" ).fetchone() version = connection.execute("SHOW server_version").fetchone() - if row != (name, "UTF8", "C") or version is None: + if ( + row is None + or row[:3] != (name, "UTF8", "C") + or not isinstance(row[3], str) + or not row[3] + or not isinstance(row[4], str) + or not row[4] + or not isinstance(row[5], str) + or not row[5] + or version is None + ): raise HarnessError(f"database identity is not UTF8/C: {row}") - return {"name": row[0], "encoding": row[1], "collation": row[2], "version": version[0]} + return { + "name": row[0], + "encoding": row[1], + "collation": row[2], + "version": version[0], + "cluster_identity": { + "system_identifier": row[5], + "data_directory": row[3], + "postmaster_started_at": row[4], + }, + } def _drop_database(admin_url: str, name: str) -> None: - with psycopg.connect(admin_url, autocommit=True) as connection: + with _database_connect(admin_url, autocommit=True) as connection: connection.execute(sql.SQL("DROP DATABASE {} WITH (FORCE)").format(sql.Identifier(name))) +def _drop_databases(admin_url: str, created: list[str]) -> None: + failures: list[str] = [] + for name in reversed(created): + try: + _drop_database(admin_url, name) + except Exception as error: # noqa: BLE001 - attempt every exact owned database + failures.append(f"{name}: {error}") + if failures: + raise HarnessError("database cleanup failed: " + "; ".join(failures)) + + def _wait_port(process: subprocess.Popen[bytes], port: int, timeout: float = 30) -> None: deadline = time.monotonic() + timeout while time.monotonic() < deadline: @@ -953,7 +999,7 @@ def _run_signing_vectors( text=True, timeout=30, ).stdout - with psycopg.connect(database_url, autocommit=True) as connection: + with _database_connect(database_url, autocommit=True) as connection: connection.execute(migration) result_path = output / "signing-vectors.json" stderr_path = output / "signing-vectors.stderr.log" @@ -978,7 +1024,7 @@ def _run_signing_vectors( raise HarnessError(f"signing vector totals changed: {report.get('totals')}") return { "status": "passed", - "database": database_identity, + "database_identity": database_identity, "result": report, "evidence": [ _retained(result_path, output), @@ -1522,7 +1568,7 @@ def _run_python_control( "reason": ( "Core control passed; the cell's full declared scenario set is not implemented" ), - "database": database_identity, + "database_identity": database_identity, "runtime": _runtime_identity(runtime, artifact), "result": result, "entrypoints": { @@ -1560,6 +1606,7 @@ def _run_python_control( "Candidate TypeScript Core reconciliation passed; the cell's full declared " "scenario set remains incomplete" ), + "database_identity": database_identity, "result": ts_gap, } ) @@ -1658,7 +1705,7 @@ def _run_ts_core_control( "cell_complete": False, "acceptance": False, "reason": "Core control passed; Managed Delivery and the full scenario set remain open", - "database": database_identity, + "database_identity": database_identity, "seller_process_identity": seller_process_identity, "seller_artifact_identity": seller_artifact_identity, "python_runtime": _runtime_identity(runtime, artifact), @@ -1696,6 +1743,7 @@ def _run_ts_core_control( if install.role == "candidate" else "rc.44 supplemental Core lane only; it is not the blocking pin" ), + "database_identity": database_identity, "seller_process_identity": seller_process_identity, "seller_artifact_identity": seller_artifact_identity, "result": ts_result, @@ -1704,6 +1752,83 @@ def _run_ts_core_control( return [py_cell, ts_cell] +def _timeout_text(value: str | bytes | None) -> str: + if value is None: + return "" + return value.decode("utf-8", errors="replace") if isinstance(value, bytes) else value + + +def _run_storyboard_process( + command: list[str], + *, + cwd: Path, + stdout_path: Path, + stderr_path: Path, + timeout_seconds: int, + shutdown_timeout_seconds: int, +) -> subprocess.CompletedProcess[str]: + process = subprocess.Popen( + command, + cwd=cwd, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + start_new_session=True, + ) + try: + stdout, stderr = process.communicate(timeout=timeout_seconds) + except subprocess.TimeoutExpired as timeout_error: + stdout = _timeout_text(timeout_error.stdout) + stderr = _timeout_text(timeout_error.stderr) + cleanup_failures: list[str] = [] + if process.poll() is None: + try: + # SIGINT gives the orchestration process a catchable exception, + # allowing its own seller/database finally blocks to settle. + os.killpg(process.pid, signal.SIGINT) + except ProcessLookupError: + pass + except OSError as error: + cleanup_failures.append(f"SIGINT: {error}") + try: + final_stdout, final_stderr = process.communicate(timeout=shutdown_timeout_seconds) + stdout = final_stdout or stdout + stderr = final_stderr or stderr + except subprocess.TimeoutExpired as term_error: + stdout = _timeout_text(term_error.stdout) or stdout + stderr = _timeout_text(term_error.stderr) or stderr + if process.poll() is None: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + except OSError as error: + cleanup_failures.append(f"SIGKILL: {error}") + try: + final_stdout, final_stderr = process.communicate(timeout=shutdown_timeout_seconds) + stdout = final_stdout or stdout + stderr = final_stderr or stderr + except subprocess.TimeoutExpired as kill_error: + stdout = _timeout_text(kill_error.stdout) or stdout + stderr = _timeout_text(kill_error.stderr) or stderr + cleanup_failures.append("process group did not exit after SIGKILL") + stdout_path.write_text(stdout, encoding="utf-8") + stderr_path.write_text(stderr, encoding="utf-8") + detail = ( + "; process-group cleanup failed: " + "; ".join(cleanup_failures) + if cleanup_failures + else "" + ) + raise HarnessError( + f"storyboard orchestration timed out after {timeout_seconds}s{detail}; " + f"see {stdout_path} and {stderr_path}" + ) from timeout_error + stdout_path.write_text(stdout, encoding="utf-8") + stderr_path.write_text(stderr, encoding="utf-8") + return subprocess.CompletedProcess(command, process.returncode, stdout, stderr) + + def _run_storyboard_server( node: Path, install: TypeScriptInstall, @@ -1720,37 +1845,36 @@ def _run_storyboard_server( orchestration_output = output / "orchestration" stdout_path = output / "orchestration.stdout.log" stderr_path = output / "orchestration.stderr.log" - completed = subprocess.run( - [ - sys.executable, - "-I", - str(STORYBOARD_ORCHESTRATION), - "--node-runtime", - str(node), - "--node-sha256", - _sha256(node), - "--typescript-install", - str(install.root), - "--typescript-tarball", - str(archive.path), - "--typescript-role", - install.role, - "--inventory-json", - str(inventory_path), - "--external-pg-admin-dsn", - admin_url, - "--output", - str(orchestration_output), - "--execute", - ], + command = [ + sys.executable, + "-I", + str(STORYBOARD_ORCHESTRATION), + "--node-runtime", + str(node), + "--node-sha256", + _sha256(node), + "--typescript-install", + str(install.root), + "--typescript-tarball", + str(archive.path), + "--typescript-role", + install.role, + "--inventory-json", + str(inventory_path), + "--external-pg-admin-dsn", + admin_url, + "--output", + str(orchestration_output), + "--execute", + ] + completed = _run_storyboard_process( + command, cwd=ROOT, - stdin=subprocess.DEVNULL, - capture_output=True, - text=True, - timeout=1_500, + stdout_path=stdout_path, + stderr_path=stderr_path, + timeout_seconds=STORYBOARD_EXECUTION_TIMEOUT_SECONDS, + shutdown_timeout_seconds=STORYBOARD_SHUTDOWN_TIMEOUT_SECONDS, ) - stdout_path.write_text(completed.stdout, encoding="utf-8") - stderr_path.write_text(completed.stderr, encoding="utf-8") result_path = orchestration_output / "results.json" if completed.returncode == 2 or not result_path.is_file(): raise HarnessError( @@ -1977,7 +2101,7 @@ def _run_candidate_python_previous_ts( "core_reporting": "passed", "actionable_unsupported": "not_triggered_for_supported_core_contract", }, - "database": database_identity, + "database_identity": database_identity, "seller_process_identity": { "pid": ready["pid"], "start_token": ready["process_start_token"], @@ -2081,7 +2205,7 @@ def _run_previous_python_candidate_ts( "released Python rc.3 rejects explicit rc.4 before transport; remaining applicable " "cell scenarios still require execution" ), - "database": database_identity, + "database_identity": database_identity, "seller_process_identity": { "pid": ready["pid"], "start_token": ready["process_start_token"], @@ -2147,10 +2271,28 @@ def _execution_credit_errors(contract: dict[str, Any], observed: dict[str, Any]) ): errors.append("seller_runtime_identity_does_not_match_cell_server") database_identity = observed.get("database_identity") - if not isinstance(database_identity, dict) or not { - "name", - "cluster_identity", - }.issubset(database_identity): + cluster_identity = ( + database_identity.get("cluster_identity") if isinstance(database_identity, dict) else None + ) + if ( + not isinstance(database_identity, dict) + or not isinstance(database_identity.get("name"), str) + or not database_identity["name"] + or not isinstance(cluster_identity, dict) + or not { + "system_identifier", + "data_directory", + "postmaster_started_at", + }.issubset(cluster_identity) + or any( + not isinstance(cluster_identity.get(field), str) or not cluster_identity[field] + for field in ( + "system_identifier", + "data_directory", + "postmaster_started_at", + ) + ) + ): errors.append("database_identity_missing") protocol = contract.get("protocol_contract") @@ -2524,8 +2666,7 @@ def main() -> None: ) finally: if not args.keep_databases: - for database in reversed(created): - _drop_database(args.pg_admin_url, database) + _drop_databases(args.pg_admin_url, created) applicability = json.loads(APPLICABILITY.read_text(encoding="utf-8")) execution_accounting = _execution_accounting(applicability, cells) diff --git a/scripts/ci/reporting_interop_matrix.py b/scripts/ci/reporting_interop_matrix.py index 84f09618f..057289ee8 100644 --- a/scripts/ci/reporting_interop_matrix.py +++ b/scripts/ci/reporting_interop_matrix.py @@ -29,6 +29,12 @@ DEFAULT_MANIFEST = Path(__file__).with_name("reporting_interop_inputs.json") REPOSITORY_ROOT = Path(__file__).resolve().parents[2] NPM_REGISTRY = "https://registry.npmjs.org/" +VERSION_COMMAND_TIMEOUT_SECONDS = 30 +PROBE_COMMAND_TIMEOUT_SECONDS = 60 +SETUP_COMMAND_TIMEOUT_SECONDS = 120 +PACKAGE_INSTALL_TIMEOUT_SECONDS = 600 +COSIGN_VERIFY_TIMEOUT_SECONDS = 120 +GITHUB_API_TIMEOUT_SECONDS = 60 MUTABLE_VERSIONS = {"latest", "next", "rc", "beta", "alpha", "canary", "main", "master", "head"} HEX_40 = re.compile(r"^[0-9a-f]{40}$") HEX_64 = re.compile(r"^[0-9a-f]{64}$") @@ -504,9 +510,10 @@ def _verify_protocol(role: str, item: Mapping[str, Any], output: Path) -> dict[s cosign = shutil.which("cosign") if not cosign: raise InputError("cosign is required to verify signed protocol inputs") - cosign_version = subprocess.run( - [cosign, "version", "--json"], check=True, text=True, capture_output=True - ).stdout.strip() + cosign_version = _run_text( + [cosign, "version", "--json"], + timeout_seconds=VERSION_COMMAND_TIMEOUT_SECONDS, + ) argv = [ cosign, "verify-blob", @@ -520,11 +527,13 @@ def _verify_protocol(role: str, item: Mapping[str, Any], output: Path) -> dict[s "https://token.actions.githubusercontent.com", str(paths["source"]), ] - completed = subprocess.run(argv, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - if completed.returncode != 0: - raise InputError(f"cosign rejected protocol {role}: {completed.stdout.strip()}") cosign_output = output / f"protocol-{role}-cosign.log" - cosign_output.write_text(completed.stdout, encoding="utf-8") + completed = _run_logged( + argv, + cwd=output, + log=cosign_output, + timeout_seconds=COSIGN_VERIFY_TIMEOUT_SECONDS, + ) return { "version": item["version"], "target_commit": item["target_commit"], @@ -541,23 +550,60 @@ def _verify_protocol(role: str, item: Mapping[str, Any], output: Path) -> dict[s } -def _run_logged(argv: list[str], *, cwd: Path, log: Path) -> subprocess.CompletedProcess[str]: - completed = subprocess.run( - argv, - cwd=cwd, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) +def _timeout_output(error: subprocess.TimeoutExpired) -> str: + output = error.stdout or "" + return output.decode("utf-8", errors="replace") if isinstance(output, bytes) else output + + +def _run_logged( + argv: list[str], + *, + cwd: Path, + log: Path, + timeout_seconds: int, +) -> subprocess.CompletedProcess[str]: + try: + completed = subprocess.run( + argv, + cwd=cwd, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + timeout=timeout_seconds, + ) + except subprocess.TimeoutExpired as error: + log.write_text(_timeout_output(error), encoding="utf-8") + raise InputError( + f"command timed out after {timeout_seconds}s: {' '.join(argv)}; see {log}" + ) from error + except OSError as error: + raise InputError(f"cannot run command {' '.join(argv)}: {error}") from error log.write_text(completed.stdout, encoding="utf-8") if completed.returncode != 0: raise InputError( - f"installed baseline command failed ({completed.returncode}): {' '.join(argv)}; " - f"see {log}" + f"command failed ({completed.returncode}): {' '.join(argv)}; " f"see {log}" ) return completed +def _run_text(argv: list[str], *, timeout_seconds: int) -> str: + try: + return subprocess.run( + argv, + check=True, + text=True, + capture_output=True, + timeout=timeout_seconds, + ).stdout.strip() + except subprocess.TimeoutExpired as error: + raise InputError(f"command timed out after {timeout_seconds}s: {' '.join(argv)}") from error + except subprocess.CalledProcessError as error: + detail = error.stderr.strip() if error.stderr else str(error) + raise InputError(f"command failed: {detail}") from error + except OSError as error: + raise InputError(f"cannot run command {' '.join(argv)}: {error}") from error + + def _retained(path: Path) -> dict[str, Any]: data = path.read_bytes() return {"path": str(path), "bytes": len(data), "sha256": _sha(data, "sha256")} @@ -593,12 +639,14 @@ def _installed_baseline( [str(uv), "venv", "--python", str(python_executable), str(venv)], cwd=install_root, log=install_root / "python-venv.log", + timeout_seconds=SETUP_COMMAND_TIMEOUT_SECONDS, ) cell_python = venv / "bin" / "python" _run_logged( [str(uv), "pip", "install", "--python", str(cell_python), str(python_wheel)], cwd=install_root, log=install_root / "python-install.log", + timeout_seconds=PACKAGE_INSTALL_TIMEOUT_SECONDS, ) python_probe = _run_logged( [ @@ -615,6 +663,7 @@ def _installed_baseline( ], cwd=install_root, log=install_root / "python-probe.json", + timeout_seconds=PROBE_COMMAND_TIMEOUT_SECONDS, ) python_probe_value = json.loads(python_probe.stdout) if ( @@ -627,9 +676,15 @@ def _installed_baseline( [str(uv), "pip", "freeze", "--python", str(cell_python)], cwd=install_root, log=install_root / "python-freeze.txt", + timeout_seconds=SETUP_COMMAND_TIMEOUT_SECONDS, ) - _run_logged([str(npm), "init", "-y"], cwd=node_root, log=install_root / "npm-init.log") + _run_logged( + [str(npm), "init", "-y"], + cwd=node_root, + log=install_root / "npm-init.log", + timeout_seconds=PROBE_COMMAND_TIMEOUT_SECONDS, + ) _run_logged( [ str(npm), @@ -642,6 +697,7 @@ def _installed_baseline( ], cwd=node_root, log=install_root / "npm-install.log", + timeout_seconds=PACKAGE_INSTALL_TIMEOUT_SECONDS, ) node_probe = _run_logged( [ @@ -660,6 +716,7 @@ def _installed_baseline( ], cwd=node_root, log=install_root / "node-probe.json", + timeout_seconds=PROBE_COMMAND_TIMEOUT_SECONDS, ) node_probe_value = json.loads(node_probe.stdout) if ( @@ -672,6 +729,7 @@ def _installed_baseline( [str(npm), "ls", "--all", "--json"], cwd=node_root, log=install_root / "npm-tree.json", + timeout_seconds=SETUP_COMMAND_TIMEOUT_SECONDS, ) retained_paths = [ @@ -693,12 +751,12 @@ def _installed_baseline( "typescript": node_probe_value, "runtime": { "python": python_probe.args[0], - "node": subprocess.run( - [str(node), "--version"], check=True, text=True, capture_output=True - ).stdout.strip(), - "npm": subprocess.run( - [str(npm), "--version"], check=True, text=True, capture_output=True - ).stdout.strip(), + "node": _run_text( + [str(node), "--version"], timeout_seconds=VERSION_COMMAND_TIMEOUT_SECONDS + ), + "npm": _run_text( + [str(npm), "--version"], timeout_seconds=VERSION_COMMAND_TIMEOUT_SECONDS + ), "os_release_sha256": _sha(Path("/etc/os-release").read_bytes(), "sha256"), }, "dependency_counts": { @@ -780,9 +838,20 @@ def verify_available( return evidence -def _run_json(argv: Iterable[str]) -> Any: +def _run_json(argv: Iterable[str], *, timeout_seconds: int) -> Any: + command = list(argv) try: - completed = subprocess.run(list(argv), check=True, text=True, capture_output=True) + completed = subprocess.run( + command, + check=True, + text=True, + capture_output=True, + timeout=timeout_seconds, + ) + except subprocess.TimeoutExpired as error: + raise InputError( + f"command timed out after {timeout_seconds}s: {' '.join(command)}" + ) from error except (OSError, subprocess.CalledProcessError) as error: detail = ( error.stderr.strip() @@ -793,7 +862,7 @@ def _run_json(argv: Iterable[str]) -> Any: try: return json.loads(completed.stdout) except json.JSONDecodeError as error: - raise InputError(f"command did not return JSON: {' '.join(argv)}") from error + raise InputError(f"command did not return JSON: {' '.join(command)}") from error def monitor_typescript(manifest: Mapping[str, Any]) -> dict[str, Any]: @@ -819,7 +888,10 @@ def monitor_typescript(manifest: Mapping[str, Any]) -> dict[str, Any]: if proposed == forbidden: raise InputError("proposed TypeScript candidate equals the forbidden prior release") - pr = _run_json(("gh", "api", f"repos/{repository}/pulls/{release_pr}")) + pr = _run_json( + ("gh", "api", f"repos/{repository}/pulls/{release_pr}"), + timeout_seconds=GITHUB_API_TIMEOUT_SECONDS, + ) body = str(pr.get("body") or "") body_version = re.search(r"@adcp/sdk@([0-9A-Za-z.-]+)", body) if not body_version or body_version.group(1) != proposed: @@ -851,7 +923,8 @@ def monitor_typescript(manifest: Mapping[str, Any]) -> dict[str, Any]: "gh", "api", f"repos/{repository}/actions/runs?head_sha={merge_commit}&event=push&per_page=100", - ) + ), + timeout_seconds=GITHUB_API_TIMEOUT_SECONDS, ) workflow_runs = _object(runs, "release workflow runs").get("workflow_runs") if not isinstance(workflow_runs, list): diff --git a/tests/test_reporting_interop_matrix.py b/tests/test_reporting_interop_matrix.py index 3cfa7eeda..4929892f4 100644 --- a/tests/test_reporting_interop_matrix.py +++ b/tests/test_reporting_interop_matrix.py @@ -4,6 +4,7 @@ import hashlib import importlib.util import json +import subprocess import sys from pathlib import Path @@ -104,6 +105,47 @@ def test_baseline_is_explicitly_non_acceptance() -> None: assert baseline["acceptance"] is False +def test_logged_command_timeout_fails_closed_and_retains_output( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + command = ["uv", "pip", "install", "fixture.whl"] + observed: dict[str, object] = {} + + def time_out(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + observed.update(kwargs) + raise subprocess.TimeoutExpired(argv, kwargs["timeout"], output=b"partial output\n") + + monkeypatch.setattr(matrix.subprocess, "run", time_out) + log = tmp_path / "install.log" + + with pytest.raises(matrix.InputError, match=r"command timed out after 7s: uv pip install"): + matrix._run_logged( + command, + cwd=tmp_path, + log=log, + timeout_seconds=7, + ) + + assert observed["timeout"] == 7 + assert log.read_text(encoding="utf-8") == "partial output\n" + + +def test_json_command_timeout_fails_closed(monkeypatch: pytest.MonkeyPatch) -> None: + command = ["gh", "api", "repos/example/project/pulls/1"] + observed: dict[str, object] = {} + + def time_out(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + observed.update(kwargs) + raise subprocess.TimeoutExpired(argv, kwargs["timeout"]) + + monkeypatch.setattr(matrix.subprocess, "run", time_out) + + with pytest.raises(matrix.InputError, match=r"command timed out after 11s: gh api"): + matrix._run_json(command, timeout_seconds=11) + + assert observed["timeout"] == 11 + + def complete_inputs() -> dict: value = inputs() value["phase"] = "acceptance" diff --git a/tests/test_reporting_storyboard_orchestration.py b/tests/test_reporting_storyboard_orchestration.py index 333d57caa..7531b442a 100644 --- a/tests/test_reporting_storyboard_orchestration.py +++ b/tests/test_reporting_storyboard_orchestration.py @@ -24,6 +24,23 @@ SPEC.loader.exec_module(orchestration) +def load_foundation_runner(monkeypatch: pytest.MonkeyPatch, name: str): + fake_psycopg = types.ModuleType("psycopg") + fake_psycopg.sql = SimpleNamespace() + fake_conninfo = types.ModuleType("psycopg.conninfo") + fake_conninfo.conninfo_to_dict = lambda _value: {} + fake_conninfo.make_conninfo = lambda **_value: "postgresql://fixture/database" + monkeypatch.setitem(sys.modules, "psycopg", fake_psycopg) + monkeypatch.setitem(sys.modules, "psycopg.conninfo", fake_conninfo) + runner_path = ROOT / "scripts/ci/reporting_interop/run_foundation_matrix.py" + spec = importlib.util.spec_from_file_location(name, runner_path) + assert spec is not None and spec.loader is not None + runner = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = runner + spec.loader.exec_module(runner) + return runner + + def inventory() -> dict: rows = [] for storyboard_id, steps, stateful, _server_kind in orchestration.REQUIRED_STORYBOARDS: @@ -416,6 +433,224 @@ def cleanup(): assert errors == ["cleanup: drop failed"] +def test_foundation_database_connect_is_bounded(monkeypatch: pytest.MonkeyPatch) -> None: + runner = load_foundation_runner(monkeypatch, "reporting_matrix_bounded_connect_test") + calls: list[tuple[str, dict[str, object]]] = [] + sentinel = object() + + def connect(dsn: str, **kwargs: object) -> object: + calls.append((dsn, kwargs)) + return sentinel + + monkeypatch.setattr(runner.psycopg, "connect", connect, raising=False) + + assert runner._database_connect("postgresql://fixture/admin", autocommit=True) is sentinel + assert calls == [ + ( + "postgresql://fixture/admin", + { + "autocommit": True, + "connect_timeout": runner.DATABASE_OPERATION_TIMEOUT_SECONDS, + "options": "-c statement_timeout=5000 -c lock_timeout=5000", + }, + ) + ] + + +def test_foundation_database_identity_matches_execution_credit_contract( + monkeypatch: pytest.MonkeyPatch, +) -> None: + runner = load_foundation_runner(monkeypatch, "reporting_matrix_database_identity_test") + executions: list[object] = [] + + class Result: + def __init__(self, row: tuple[str, ...] | None = None) -> None: + self.row = row + + def fetchone(self) -> tuple[str, ...] | None: + return self.row + + class Connection: + def __init__(self, rows: list[tuple[str, ...] | None]) -> None: + self.rows = iter(rows) + + def __enter__(self): + return self + + def __exit__(self, *_args: object) -> None: + return None + + def execute(self, statement: object) -> Result: + executions.append(statement) + return Result(next(self.rows)) + + class Statement: + def format(self, *_args: object): + return self + + connections = iter( + [ + Connection([None]), + Connection( + [ + ( + "cell_database", + "UTF8", + "C", + "/owned/postgres/data", + "2026-09-26 12:34:56+00", + "7491234567890123456", + ), + ("16.14",), + ] + ), + ] + ) + monkeypatch.setattr( + runner, + "sql", + SimpleNamespace(SQL=lambda _value: Statement(), Identifier=lambda value: value), + ) + monkeypatch.setattr( + runner, + "_database_connect", + lambda _dsn, *, autocommit: next(connections), + ) + monkeypatch.setattr( + runner, + "_database_url", + lambda _admin_url, _name: "postgresql://fixture/cell_database", + ) + + identity = runner._create_database("postgresql://fixture/admin", "cell_database") + + assert identity == { + "name": "cell_database", + "encoding": "UTF8", + "collation": "C", + "version": "16.14", + "cluster_identity": { + "system_identifier": "7491234567890123456", + "data_directory": "/owned/postgres/data", + "postmaster_started_at": "2026-09-26 12:34:56+00", + }, + } + assert len(executions) == 3 + + contract = { + "id": "q1", + "contract_id": "Q1", + "server": {"language": "typescript", "package_role": "candidate"}, + } + observed = { + "id": "q1", + "target_contract_id": "Q1", + "execution_attempted": True, + "required_cell_credit": True, + "seller_process_identity": { + "pid": 1, + "start_token": "1", + "startup_proof_sha256": "a" * 64, + }, + "seller_artifact_identity": { + "language": "typescript", + "package_role": "candidate", + "runtime_identity_sha256": "b" * 64, + "artifact_identity_sha256": "c" * 64, + }, + "database_identity": identity, + } + assert "database_identity_missing" not in runner._execution_credit_errors(contract, observed) + observed["required_cell_credit"] = False + assert "observed_control_disclaims_required_credit" in runner._execution_credit_errors( + contract, observed + ) + + +def test_foundation_database_cleanup_attempts_every_owned_name( + monkeypatch: pytest.MonkeyPatch, +) -> None: + runner = load_foundation_runner(monkeypatch, "reporting_matrix_cleanup_test") + attempted: list[str] = [] + + def drop(_admin_url: str, name: str) -> None: + attempted.append(name) + if name != "middle": + raise RuntimeError(f"cannot drop {name}") + + monkeypatch.setattr(runner, "_drop_database", drop) + + with pytest.raises( + runner.HarnessError, + match=r"database cleanup failed: last: cannot drop last; first: cannot drop first", + ): + runner._drop_databases( + "postgresql://fixture/admin", + ["first", "middle", "last"], + ) + + assert attempted == ["last", "middle", "first"] + + +def test_foundation_storyboard_timeout_terminates_owned_process_group( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + runner = load_foundation_runner(monkeypatch, "reporting_matrix_storyboard_timeout_test") + popen_kwargs: dict[str, object] = {} + timeouts: list[int] = [] + signals: list[tuple[int, signal.Signals]] = [] + + class Process: + pid = 4242 + returncode: int | None = None + + def communicate(self, *, timeout: int): + timeouts.append(timeout) + if len(timeouts) < 3: + raise subprocess.TimeoutExpired( + ["storyboard"], + timeout, + output=b"partial stdout\n", + stderr=b"partial stderr\n", + ) + return ("final stdout\n", "final stderr\n") + + def poll(self) -> int | None: + return self.returncode + + process = Process() + + def popen(_command: list[str], **kwargs: object) -> Process: + popen_kwargs.update(kwargs) + return process + + def killpg(pid: int, sent_signal: signal.Signals) -> None: + signals.append((pid, sent_signal)) + if sent_signal == signal.SIGKILL: + process.returncode = -signal.SIGKILL + + monkeypatch.setattr(runner.subprocess, "Popen", popen) + monkeypatch.setattr(runner.os, "killpg", killpg) + stdout = tmp_path / "storyboard.stdout.log" + stderr = tmp_path / "storyboard.stderr.log" + + with pytest.raises(runner.HarnessError, match="storyboard orchestration timed out"): + runner._run_storyboard_process( + ["storyboard"], + cwd=tmp_path, + stdout_path=stdout, + stderr_path=stderr, + timeout_seconds=3, + shutdown_timeout_seconds=2, + ) + + assert popen_kwargs["start_new_session"] is True + assert signals == [(4242, signal.SIGINT), (4242, signal.SIGKILL)] + assert timeouts == [3, 2, 2] + assert stdout.read_text(encoding="utf-8") == "final stdout\n" + assert stderr.read_text(encoding="utf-8") == "final stderr\n" + + def test_storyboard_timeout_retains_partial_output_and_settles_child( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1576,7 +1811,14 @@ def test_foundation_accounting_rejects_id_only_rc4_and_negative_skew_credit( "runtime_identity_sha256": "c" * 64, "artifact_identity_sha256": "d" * 64, }, - "database_identity": {"name": "database-1", "cluster_identity": "cluster-1"}, + "database_identity": { + "name": "database-1", + "cluster_identity": { + "system_identifier": "cluster-1", + "data_directory": "/owned/cluster-1", + "postmaster_started_at": "2026-09-26 00:00:00+00", + }, + }, "cell_complete": True, } rows = runner._execution_accounting( @@ -1670,7 +1912,14 @@ def test_foundation_accounting_requires_fresh_identity_and_positive_semantics( "runtime_identity_sha256": "c" * 64, "artifact_identity_sha256": "d" * 64, }, - "database_identity": {"name": "database-1", "cluster_identity": "cluster-1"}, + "database_identity": { + "name": "database-1", + "cluster_identity": { + "system_identifier": "cluster-1", + "data_directory": "/owned/cluster-1", + "postmaster_started_at": "2026-09-26 00:00:00+00", + }, + }, "observed_protocol": "3.2.0-rc.6", "polarity": "positive_semantic", "cell_complete": True, From 8e691e2f7885ea959bd8f6bb9c470d6bf23f8250 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 26 Sep 2026 23:26:27 +0000 Subject: [PATCH 6/7] fix(reporting): settle timed-out process groups --- .../run_foundation_matrix.py | 32 +++++------ ...test_reporting_storyboard_orchestration.py | 55 ++++++++++++++++++- 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/scripts/ci/reporting_interop/run_foundation_matrix.py b/scripts/ci/reporting_interop/run_foundation_matrix.py index dc5ad07ae..c275128ff 100644 --- a/scripts/ci/reporting_interop/run_foundation_matrix.py +++ b/scripts/ci/reporting_interop/run_foundation_matrix.py @@ -1782,15 +1782,16 @@ def _run_storyboard_process( stdout = _timeout_text(timeout_error.stdout) stderr = _timeout_text(timeout_error.stderr) cleanup_failures: list[str] = [] - if process.poll() is None: - try: - # SIGINT gives the orchestration process a catchable exception, - # allowing its own seller/database finally blocks to settle. - os.killpg(process.pid, signal.SIGINT) - except ProcessLookupError: - pass - except OSError as error: - cleanup_failures.append(f"SIGINT: {error}") + try: + # The direct child may already have exited while a descendant keeps + # its pipes open, so group cleanup must not depend on process.poll(). + # SIGINT gives the orchestration process a catchable exception, + # allowing its own seller/database finally blocks to settle. + os.killpg(process.pid, signal.SIGINT) + except ProcessLookupError: + pass + except OSError as error: + cleanup_failures.append(f"SIGINT: {error}") try: final_stdout, final_stderr = process.communicate(timeout=shutdown_timeout_seconds) stdout = final_stdout or stdout @@ -1798,13 +1799,12 @@ def _run_storyboard_process( except subprocess.TimeoutExpired as term_error: stdout = _timeout_text(term_error.stdout) or stdout stderr = _timeout_text(term_error.stderr) or stderr - if process.poll() is None: - try: - os.killpg(process.pid, signal.SIGKILL) - except ProcessLookupError: - pass - except OSError as error: - cleanup_failures.append(f"SIGKILL: {error}") + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + except OSError as error: + cleanup_failures.append(f"SIGKILL: {error}") try: final_stdout, final_stderr = process.communicate(timeout=shutdown_timeout_seconds) stdout = final_stdout or stdout diff --git a/tests/test_reporting_storyboard_orchestration.py b/tests/test_reporting_storyboard_orchestration.py index 7531b442a..fbd2cb0a6 100644 --- a/tests/test_reporting_storyboard_orchestration.py +++ b/tests/test_reporting_storyboard_orchestration.py @@ -592,7 +592,7 @@ def drop(_admin_url: str, name: str) -> None: assert attempted == ["last", "middle", "first"] -def test_foundation_storyboard_timeout_terminates_owned_process_group( +def test_foundation_storyboard_timeout_kills_sigint_resistant_descendant_group( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: runner = load_foundation_runner(monkeypatch, "reporting_matrix_storyboard_timeout_test") @@ -626,7 +626,11 @@ def popen(_command: list[str], **kwargs: object) -> Process: def killpg(pid: int, sent_signal: signal.Signals) -> None: signals.append((pid, sent_signal)) - if sent_signal == signal.SIGKILL: + if sent_signal == signal.SIGINT: + # The direct child exits, but a resistant descendant keeps the + # captured pipes open until the process group is killed. + process.returncode = 0 + else: process.returncode = -signal.SIGKILL monkeypatch.setattr(runner.subprocess, "Popen", popen) @@ -651,6 +655,53 @@ def killpg(pid: int, sent_signal: signal.Signals) -> None: assert stderr.read_text(encoding="utf-8") == "final stderr\n" +def test_foundation_storyboard_timeout_signals_group_after_parent_exits( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + runner = load_foundation_runner(monkeypatch, "reporting_matrix_exited_parent_timeout_test") + signals: list[tuple[int, signal.Signals]] = [] + + class Process: + pid = 4343 + returncode = 1 + calls = 0 + + def communicate(self, *, timeout: int): + self.calls += 1 + if self.calls == 1: + raise subprocess.TimeoutExpired( + ["storyboard"], + timeout, + output=b"parent exited\n", + stderr=b"descendant retained pipe\n", + ) + return ("settled stdout\n", "settled stderr\n") + + def poll(self) -> int: + return self.returncode + + process = Process() + monkeypatch.setattr(runner.subprocess, "Popen", lambda *_args, **_kwargs: process) + monkeypatch.setattr( + runner.os, + "killpg", + lambda pid, sent_signal: signals.append((pid, sent_signal)), + ) + + with pytest.raises(runner.HarnessError, match="storyboard orchestration timed out"): + runner._run_storyboard_process( + ["storyboard"], + cwd=tmp_path, + stdout_path=tmp_path / "stdout.log", + stderr_path=tmp_path / "stderr.log", + timeout_seconds=3, + shutdown_timeout_seconds=2, + ) + + assert signals == [(4343, signal.SIGINT)] + assert process.calls == 2 + + def test_storyboard_timeout_retains_partial_output_and_settles_child( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: From e1cac81d7c585fb95668d2d16e6f891866f39879 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 26 Sep 2026 23:36:07 +0000 Subject: [PATCH 7/7] fix(reporting): support unprivileged database identity --- .../run_foundation_matrix.py | 11 +-- ...t_reporting_interop_foundation_database.py | 80 +++++++++++++++++++ ...test_reporting_storyboard_orchestration.py | 4 - 3 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 tests/conformance/reporting/test_reporting_interop_foundation_database.py diff --git a/scripts/ci/reporting_interop/run_foundation_matrix.py b/scripts/ci/reporting_interop/run_foundation_matrix.py index c275128ff..a08a63c8b 100644 --- a/scripts/ci/reporting_interop/run_foundation_matrix.py +++ b/scripts/ci/reporting_interop/run_foundation_matrix.py @@ -469,7 +469,7 @@ def _create_database(admin_url: str, name: str) -> dict[str, Any]: with _database_connect(url, autocommit=False) as connection: row = connection.execute( "SELECT current_database(), pg_encoding_to_char(encoding), datcollate, " - "current_setting('data_directory'), pg_postmaster_start_time()::text, " + "pg_postmaster_start_time()::text, " "(SELECT system_identifier::text FROM pg_control_system()) " "FROM pg_database WHERE datname=current_database()" ).fetchone() @@ -481,8 +481,6 @@ def _create_database(admin_url: str, name: str) -> dict[str, Any]: or not row[3] or not isinstance(row[4], str) or not row[4] - or not isinstance(row[5], str) - or not row[5] or version is None ): raise HarnessError(f"database identity is not UTF8/C: {row}") @@ -492,9 +490,8 @@ def _create_database(admin_url: str, name: str) -> dict[str, Any]: "collation": row[2], "version": version[0], "cluster_identity": { - "system_identifier": row[5], - "data_directory": row[3], - "postmaster_started_at": row[4], + "system_identifier": row[4], + "postmaster_started_at": row[3], }, } @@ -2281,14 +2278,12 @@ def _execution_credit_errors(contract: dict[str, Any], observed: dict[str, Any]) or not isinstance(cluster_identity, dict) or not { "system_identifier", - "data_directory", "postmaster_started_at", }.issubset(cluster_identity) or any( not isinstance(cluster_identity.get(field), str) or not cluster_identity[field] for field in ( "system_identifier", - "data_directory", "postmaster_started_at", ) ) diff --git a/tests/conformance/reporting/test_reporting_interop_foundation_database.py b/tests/conformance/reporting/test_reporting_interop_foundation_database.py new file mode 100644 index 000000000..d2759493b --- /dev/null +++ b/tests/conformance/reporting/test_reporting_interop_foundation_database.py @@ -0,0 +1,80 @@ +"""Real-Postgres contract checks for the reporting interop harness database owner.""" + +from __future__ import annotations + +import importlib.util +import os +import secrets +import sys +from pathlib import Path + +import pytest + +psycopg = pytest.importorskip("psycopg") +sql = pytest.importorskip("psycopg.sql") +conninfo = pytest.importorskip("psycopg.conninfo") + +TEST_URL = os.environ.get("ADCP_PG_TEST_URL") +if not TEST_URL: + pytest.skip( + "ADCP_PG_TEST_URL not set — skipping interop harness Postgres tests", + allow_module_level=True, + ) + +ROOT = Path(__file__).resolve().parents[3] +RUNNER_PATH = ROOT / "scripts/ci/reporting_interop/run_foundation_matrix.py" + + +def _load_runner(): + spec = importlib.util.spec_from_file_location( + "reporting_interop_foundation_database_test", RUNNER_PATH + ) + assert spec is not None and spec.loader is not None + runner = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = runner + spec.loader.exec_module(runner) + return runner + + +def test_create_database_supports_createdb_non_superuser() -> None: + """Cluster identity must not require pg_read_all_settings or superuser.""" + + runner = _load_runner() + suffix = secrets.token_hex(6) + role = f"adcp_interop_role_{suffix}" + database = f"adcp_interop_db_{suffix}" + password = secrets.token_urlsafe(24) + fields = conninfo.conninfo_to_dict(TEST_URL) + fields.update(user=role, password=password) + role_dsn = conninfo.make_conninfo(**fields) + + with psycopg.connect(TEST_URL, autocommit=True) as admin: + admin.execute( + sql.SQL("CREATE ROLE {} LOGIN CREATEDB PASSWORD {}").format( + sql.Identifier(role), sql.Literal(password) + ) + ) + try: + identity = runner._create_database(role_dsn, database) + + assert identity["name"] == database + assert identity["encoding"] == "UTF8" + assert identity["collation"] == "C" + assert set(identity["cluster_identity"]) == { + "system_identifier", + "postmaster_started_at", + } + assert identity["cluster_identity"]["system_identifier"] + assert identity["cluster_identity"]["postmaster_started_at"] + with psycopg.connect(TEST_URL, autocommit=True) as admin: + role_row = admin.execute( + "SELECT rolsuper, rolcreatedb FROM pg_roles WHERE rolname=%s", + (role,), + ).fetchone() + assert role_row == (False, True) + finally: + with psycopg.connect(TEST_URL, autocommit=True) as admin: + admin.execute( + sql.SQL("DROP DATABASE IF EXISTS {} WITH (FORCE)").format(sql.Identifier(database)) + ) + admin.execute(sql.SQL("DROP ROLE IF EXISTS {}").format(sql.Identifier(role))) diff --git a/tests/test_reporting_storyboard_orchestration.py b/tests/test_reporting_storyboard_orchestration.py index fbd2cb0a6..afff7fe0b 100644 --- a/tests/test_reporting_storyboard_orchestration.py +++ b/tests/test_reporting_storyboard_orchestration.py @@ -497,7 +497,6 @@ def format(self, *_args: object): "cell_database", "UTF8", "C", - "/owned/postgres/data", "2026-09-26 12:34:56+00", "7491234567890123456", ), @@ -531,7 +530,6 @@ def format(self, *_args: object): "version": "16.14", "cluster_identity": { "system_identifier": "7491234567890123456", - "data_directory": "/owned/postgres/data", "postmaster_started_at": "2026-09-26 12:34:56+00", }, } @@ -1866,7 +1864,6 @@ def test_foundation_accounting_rejects_id_only_rc4_and_negative_skew_credit( "name": "database-1", "cluster_identity": { "system_identifier": "cluster-1", - "data_directory": "/owned/cluster-1", "postmaster_started_at": "2026-09-26 00:00:00+00", }, }, @@ -1967,7 +1964,6 @@ def test_foundation_accounting_requires_fresh_identity_and_positive_semantics( "name": "database-1", "cluster_identity": { "system_identifier": "cluster-1", - "data_directory": "/owned/cluster-1", "postmaster_started_at": "2026-09-26 00:00:00+00", }, },