@openwop/openwop-conformance 2.37.0 → 2.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.37.1] — 2026-09-24 — records that say why, and identities a host must remember minted fresh per run
4
+
5
+ - **`0213.in-flight-loser-outcome` no longer blocks a host that replays every loser.** `v2-idempotency-in-flight` ended with `softSkip('blocked', …)` when all five same-key creates succeeded. RFC 0213 §B explicitly permits that outcome: a loser MAY wait and receive the winner's final outcome, marked `OpenWOP-Idempotent-Replay: true`. The file's own header already promised `partial-witness`. Blocking denied certification (RFC 0168 §E.1) to every host fast enough, or serial enough, that no loser was ever in flight. It was measured on the v2-reference host with the published 2.37.0 (loopback rehearsal: all 5 answers were successes, 4 of them marked replays). The note is now `inapplicable`, which at major 2 records the row as a partial witness after the leg's assertions. Certification counts it, and the acceptance predicate still refuses it, because the 409 branch was never observed.
6
+ - **A failing assertion reaches the record with its message and its case name, and the per-`it` row of a multi-leg requirement survives.** MEASURED, `v2-run-bulk-cancel.test.ts` on a tier-2 host, both rows from ONE run: `openwop.floor.v2-run-bulk-cancel` `executed-fail`, 10 assertions, detail "one or more assertions in the file failed"; `openwop.requirement.0170.run-bulk-cancel` `executed-pass`, 3 assertions. No case name, no message, nothing else in the record for that file — the host operator could not diagnose it and hand-probed production. Two independent faults. (1) `fileDisposition`'s `executed-fail` detail was that FIXED STRING; it is now `failureDetail(...)` — the failing case titles (first three, then `+N more`) plus the first failure's message — and `resolveItRecord` names the leg in the per-`it` row's detail too. `setup.ts` collects `task.name` + the first error in `afterEach`. (2) The file has two `it`s that both hand `req()` one module-level `const ID`; leg 1 passed with 3 assertions and leg 2 failed on its 7th, and leg 2's row hit `recordRequirement`'s one-disposition-per-run throw, which `setup.ts`'s "never fail a test for bookkeeping" `catch` swallowed — so the verdict reached neither the in-memory ledger nor the JSONL sink. `recordRequirement` gains `fold`, used only by the per-`it` path: the surviving row is the LEAST certifiable leg (the rank `readLedgerFile` already applies across workers), carrying that leg's detail, with the legs' assertion counts summed; a scenario that classifies ITSELF twice still throws. 27 scenario files share one explicit id across several `it`s, so this was never one file's bug — `scripts/check-req-only.mjs` rule (d) now resolves a `const` handed to `req()` (it compared only call-site literals and was blind to the form nearly every scenario uses) and no longer flags the shared-id pattern the fold makes safe. **Why 7 of the 10 assertions looked unattributed, stated rather than gated:** they were not. All ten carry `req(ID, …)`, and attribution is per-`it`, not per-assertion — `req()` sets one `explicitId` that `setup.ts` takes once per test, and `assertionCount` counts `expect` calls whatever their message. The 3-vs-10 was the discarded leg, not a missing id. (The suite does hold 1,281 `expect`s with no message at all, against 4,824 carrying `req(...)`; they cost the reader a sentence, never the row its id, and the case name now carries alongside vitest's own diff. The reasoning is recorded in `check-req-only.mjs` rather than turned into a 1,281-site sweep.) Sabotage, host-free: one assertion flipped in `v2-payload-seats-0186.test.ts` (3 `it`s, one shared `const ID`, same shape as the bulk-cancel file) reproduces the reported pair exactly at `origin/main` — scenario row `executed-fail` / "one or more assertions in the file failed", requirement row `executed-pass`, failing leg absent — and after the fix the requirement row is `executed-fail` naming `"§A.3 ApprovalData.onTimeout is a closed enum and the def stays closed"` with the full `req()` message, and the file row names the case and quotes it.
7
+ - **Four webhook scenarios stop registering one byte-identical destination, and a zero that is provably unmeasured stops convicting the host.** `v2-bound-id-kinds`, `v2-webhook-delivery-shape`, `v2-webhook-durable-delivery` and `webhook-signed-delivery` each bound `OPENWOP_WEBHOOK_RECEIVER_PORT` and registered `resolveRegistrationUrl(...)`, which returns `OPENWOP_WEBHOOK_RECEIVER_URL` VERBATIM — so on a tunnelled cut all four subscriptions pointed at ONE URL. A webhook subscription is durable host-side state that keeps delivering, and RETRYING, after the file that made it has finished, so whichever receiver held the port read the leftovers as its own traffic — and `v2-webhook-durable-delivery` answers 500 BY DESIGN. Both symptoms were already recorded in the tree as workarounds: `v2-webhook-delivery-shape`'s receiver carried "the tunnel forwards to the PINNED port — held by the other receiver, which answers 500 by design — so this file's `deliveries` stays empty, its legs soft-skip, and the rows resolve `executed-pass`", and `v2-webhook-durable-delivery` filters by subscription because "a tier-2 host measured 6 attempts against a maxAttempts of 5". New `src/lib/scoped-receiver.ts` composes the two halves the tree already had rather than reimplementing either: ROUTING is openwop#1520's `front-mux` (each receiver registers its handler under its nonce and calls `routeFronted` first, so several can be alive behind one front), IDENTITY is the nonce made UNCONDITIONAL — `frontedEndpoint` omits it for the pinned-port owner, which is enough for a live listener and not for a subscription that outlives its exercise. A retry for a finished exercise now addresses a nonce nobody serves, is answered 404, and is counted `foreign()` instead of absorbed. `unservedDestination()` does the same for a leg that needs the mint and no delivery (`v2-bound-id-kinds` leg 1). Zeroes: where other traffic DID reach the listener, the path from host to process demonstrably works and the absence is of this exercise's IDENTITY, so the leg records `blocked` with `noDeliveryCause(...)` naming the address, the nonce and the foreign count — `blocked` denies certification exactly as a failure does (RFC 0168 §E.1), and a genuinely mis-wired front produces no traffic at all and still fails. `src/lib/scoped-receiver.test.ts` pins it host-free (8 cases: distinct destinations, the nonce surviving a front, a sibling's traffic routed to the sibling, a finished exercise's retry refused, a nonce-less stranger refused, own traffic delivered prefix-stripped, and both readings of a zero); removing the nonce from the destination reds 3 of them. The `receiverBinding` source guard in `webhook-receiver.test.ts` is rewritten to accept either shape and gains two cases: the shared helper must itself bind through `receiverBinding()`, and no listed scenario may mint its destination with `resolveRegistrationUrl`. Not witnessed against a host: no tunnelled cut was available, and no timeout or window was widened.
8
+ - **Two RFC 0213 requirement ids were cited from one `it()`, so the winner id was recorded on no host.** `v2-idempotency-in-flight.test.ts` cited `0213.in-flight-one-winner` and `0213.in-flight-loser-outcome` from a single `it`. The ledger keys on the id and `setup.ts` takes ONE `explicitId` per test, so only the last one cited got a row: the winner clause — "of concurrent same-key requests a host MUST process exactly one" — was measured on every host that ran the file and recorded on none of them, and nothing said so. Found by `check-req-only.mjs` rule (d) the moment it learned to resolve a `const` handed to `req()`, which is the same sharpening this cycle made for the attribution fix; the rule had compared only call-site literals, so the violation was invisible when the file was written. Split into two `it`s over ONE race: the record is in flight only while the winning create is being handled, so driving the race twice would measure two unrelated races and halve the chance that either overlaps, so it is driven once and memoised and both legs await the same result (the `v2-subject-link-record` pattern). No assertion, gate or timeout changed. The no-overlap soft-skip now lands on the loser id it is ABOUT, so the winner clause keeps its verdict instead of losing it to that return — a strictly better row on a host with no overlap, which is every host that answers create in milliseconds. That soft-skip's DISPOSITION is openwop#1525's, taken verbatim on merge: `inapplicable` (→ `executed-pass` + `partial-witness`), not `blocked`, because §B permits a loser to wait and receive a marked final outcome and `blocked` would deny certification to a host that did nothing wrong. The two changes compose — #1525 fixed what the row says, this one fixed which id says it.
9
+ - **A third fixed dedup key, and the source guard that would have caught it.** `trigger-stream-cdc-sources.test.ts` handed the bridge seam the literal `'events:3:99001'` and then asserted `deliveredCount === 1 || outcome === 'delivered'` — the identical defect as the bullet below, one file over, and it survived that sweep because NOTHING WAS CHECKING. §F.5 reuses §C-1's dedup window verbatim and that window is a >=24h FLOOR, so the second run of the file against the same host, any time that day, hands the bridge broker coordinates it has already delivered; a conformant host collapses the exercise into the first run's outcome and the assertion convicts it. Cold host passes, warm host fails. New `freshStreamDedupKey()` mints the OFFSET and keeps `(topic, partition)` — not `freshDedupKey`, because §F.5 says a stream event's key SHOULD derive from `(topic, partition, offset)` and the leg's own `req()` message asserts over exactly that keying; an opaque token would make the message describe something the call no longer does. The repetition §C-1 is about happens INSIDE the seam's `scenario: 'dedup'`, so the clause is untouched. New `src/lib/trigger-dedup-key.test.ts` pins it host-free and guards the CALL SITES, which is where a regression would reappear: no file that drives `driveDelivery` may spell a `dedupKey` as a literal. Sabotage, both spellings: reintroducing the literal as `dedupKey: 'events:3:99001'` (the original defect) and as `const dedupKey = 'events:3:99001'` each red the guard, naming the file and line. The first draft of that guard matched only the property form and the `const` sabotage PASSED it — the regex is one alternation over `[:=]` for that reason, recorded here because a guard that misses the spelling a reader would reach for is not a guard. Files that mention `dedupKey` without driving the seam are excluded deliberately: in `trigger-bridge-shape` it is a field in an offline AJV sample, durable nowhere.
10
+ - **The identity enumeration, re-run against the merged tree, and what it did NOT find.** Every other class was already per-exercise or is covered elsewhere, so no second mechanism is added for any of them: Layer-1 `Idempotency-Key`s are minted fresh at all four call sites (`idempotency`, `idempotencyRetry`, `v2-idempotency-key-grammar`, `pause-resume`); the whole host-surface family (`kv`, `blob`, `table`, `search`, `vector`, `sql`, `queueBus`) already mints its key / table / index / namespace / stream per exercise; `v2-subject-link-record` already mints its `externalId`; `replay-fanout-suppression` binds an EPHEMERAL port rather than the pinned one, so its destination is distinct by construction; and the scenario-owned A2A peers and MCP servers are `front-mux`'s (openwop#1520), which nonce-paths each fake behind the one public front. One row was rehearsed rather than fixed: `v2-webhook-message-id-stable` came back `executed-pass` on a loopback run against the PUBLISHED 2.37.0 with its `0201.message-id-stable` requirement row classified — no unclassified return. One run is not proof and the single scenario is being re-run before the public cut, but it is consistent with openwop#1513 and #1520 having already covered it, so NO mechanism is added for it here: not reproduced on 2.37.0, plausibly covered by `front-mux` / `effect-receiver`. **Owed, not fixed here:** `lib/oauth-as-double.ts` derives its ISSUER identity from `resolvePublicFront(frontEnv, local)`, and three files — `v2-oauth-mcp-reach-discovery`, `v2-oauth-client-pkce-state-iss`, `v2-credential-interrupt` — start a double on the same default `OPENWOP_OAUTH_AS_URL`, so on a tunnelled cut all three advertise ONE issuer and only the holder of the pinned port is reached. That is the front-mux shape for a fake front-mux does not cover, and it sits on the public-front routing another session is changing in the same cycle (openwop#1524, the per-instance `kid` in `lib/oidc-issuer.ts`), so it is stated rather than touched. Unreproduced: no tunnelled cut was available, so it is read off the code.
11
+ - **Two more fixed identities against state a host is REQUIRED to remember.** `trigger-bridge-delivery` handed the bridge the literal `'conformance-dedup-key'`, and `trigger-bridge.md` §C-1 makes the dedup window a >=24h FLOOR — so the second run of the file against the same host, any time that day, hands it a key already delivered; a conformant host collapses the exercise into the first run's outcome, emits zero `delivered` attempts under this run's id, and the `=== 1` convicts it. Cold host passes, warm host fails. `freshDedupKey()` mints one per exercise; the repetition §C-1 is about happens inside `driveDelivery`'s `scenario: 'dedup'`, so the clause is untouched. `auth-subject-link` provisioned the literal `externalId` `'idp-op-8f3a'` into the OPERATOR's SCIM directory and then DEACTIVATED it, so a second run asserts "authenticates before deactivation" against a subject the host is correct to refuse; minted per exercise now. The SCIM leg is opt-in on two env vars that no cut sets, so it is read off the code and unwitnessed — stated rather than claimed.
12
+ - **Version moved ahead of publication.** `@openwop/openwop-conformance` and its exact-pinned peer `@openwop/spec-artifacts` move to `2.37.1` because `2.37.0` is published. Published on the `v2.37.1` corpus tag.
13
+
3
14
  ## [2.37.0] — 2026-09-23 — rows that measured nothing now measure: a suite fake reachable through the public front, one effect identity per leg, the MRTR key that can tell
4
15
 
5
16
  - **Every synthetic OIDC issuer publishes its own `kid`, so two scenarios at one issuer URL no longer collide in a host's JWKS cache.** `lib/oidc-issuer.ts` generated a fresh key per `createSyntheticOIDCIssuer` instance but named it `openwop-conformance-key-${rotationCounter}` with a per-instance counter — so EVERY instance published `openwop-conformance-key-1`. `v2-oidc-id-token-audience` and `v2-lane-exp-only-bound` both publish at `OPENWOP_TEST_OIDC_ISSUER_URL`; a host that caches JWKS by `kid` (RFC 7517 §4.5: a kid identifies one key) verified the second scenario's tokens against the first scenario's cached key and refused its VALID control as `invalid_signature`. Reported and measured by openwop-app-ce on openwop-app, suite 2.36.1: `v2-lane-exp-only-bound` 4/4 red in a full run (control included), 4/4 green filtered — an order-dependent red that reads as a host failure on RFC 0210's rows. The kid is now `openwop-conformance-key-<per-instance hex>-<rotation>`. New self-test: a kid-caching verifier (re-fetch only on an unknown kid) must accept both instances' valid tokens at one URL; reverting to the shared kid reds it.
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  # --legacy-peer-deps is REQUIRED, not optional: the exact peer pin is what npm's
12
12
  # default resolver refuses. npm 10.9 fails outright with
13
13
  # "Cannot read properties of null (reading 'edgesOut')" — use npm >= 11.
14
- npm install --legacy-peer-deps @openwop/openwop-conformance@2.37.0 @openwop/spec-artifacts@2.37.0
14
+ npm install --legacy-peer-deps @openwop/openwop-conformance@2.37.1 @openwop/spec-artifacts@2.37.1
15
15
  # or run without install:
16
16
  npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
17
17
  ```
@@ -47,10 +47,26 @@ const journal = [];
47
47
  * dispositions throws: RFC 0148 §A says **exactly one** disposition per
48
48
  * requirement, and a silent last-write-wins would let a later soft-skip
49
49
  * overwrite an earlier real failure — the failure mode in reverse.
50
+ *
51
+ * `extras.fold` is the one exception, for the per-`it` rows `setup.ts` records
52
+ * when several `it` legs witness ONE requirement id; see its docblock below.
53
+ */
54
+ /**
55
+ * How certifiable each disposition is, least first. `readLedgerFile` has always
56
+ * resolved a cross-worker disagreement this way — "one worker said it failed"
57
+ * outranks "another said it passed", and an unresolvable disagreement must never
58
+ * round toward certification. `fold` below applies the SAME rule in-worker.
50
59
  */
60
+ const CERTIFIABILITY_RANK = {
61
+ 'executed-fail': 0,
62
+ blocked: 1,
63
+ 'executed-pass': 2,
64
+ skipped: 3,
65
+ inapplicable: 4,
66
+ };
51
67
  export function recordRequirement(requirementId, disposition, detail, extras) {
52
68
  const prior = ledger.get(requirementId);
53
- if (prior !== undefined && prior.disposition !== disposition) {
69
+ if (prior !== undefined && prior.disposition !== disposition && extras?.fold !== true) {
54
70
  throw new Error(`RFC 0148 §A: ${requirementId} already recorded as '${prior.disposition}', now '${disposition}'. ` +
55
71
  'Exactly one disposition per requirement per run.');
56
72
  }
@@ -58,7 +74,7 @@ export function recordRequirement(requirementId, disposition, detail, extras) {
58
74
  throw new Error(`RFC 0148 §A: ${requirementId} recorded as '${disposition}' without a reason. ` +
59
75
  'Anything other than executed-pass MUST say why, or the ledger records an outcome nobody can act on.');
60
76
  }
61
- const entry = {
77
+ let entry = {
62
78
  requirementId,
63
79
  disposition,
64
80
  ...(detail === undefined ? {} : { detail }),
@@ -66,6 +82,27 @@ export function recordRequirement(requirementId, disposition, detail, extras) {
66
82
  ...(extras?.scenarioFile === undefined ? {} : { scenarioFile: extras.scenarioFile }),
67
83
  ...(extras?.evidence === undefined || disposition !== 'executed-pass' ? {} : { evidence: extras.evidence }),
68
84
  };
85
+ if (prior !== undefined && extras?.fold === true) {
86
+ // The least-certifiable leg wins the disposition and keeps its own detail;
87
+ // a tie keeps whichever side actually said something. Counts sum, because
88
+ // both legs really did assert against the target for this one requirement.
89
+ const keepPrior = CERTIFIABILITY_RANK[prior.disposition] <= CERTIFIABILITY_RANK[disposition];
90
+ const winner = keepPrior ? prior : entry;
91
+ const loser = keepPrior ? entry : prior;
92
+ const count = (prior.assertionCount ?? 0) + (extras?.assertionCount ?? 0);
93
+ const keptDetail = winner.detail ?? loser.detail;
94
+ const hasCount = prior.assertionCount !== undefined || extras?.assertionCount !== undefined;
95
+ entry = {
96
+ requirementId,
97
+ disposition: winner.disposition,
98
+ ...(keptDetail === undefined ? {} : { detail: keptDetail }),
99
+ ...(hasCount ? { assertionCount: count } : {}),
100
+ ...(winner.scenarioFile === undefined ? {} : { scenarioFile: winner.scenarioFile }),
101
+ // `evidence` is only meaningful on a pass; a fold that lands anywhere
102
+ // else drops it, exactly as the constructor above does.
103
+ ...(winner.disposition === 'executed-pass' && winner.evidence !== undefined ? { evidence: winner.evidence } : {}),
104
+ };
105
+ }
69
106
  ledger.set(requirementId, entry);
70
107
  journal.push(entry);
71
108
  // File sink (RFC 0148 acceptance item 2, S6). The in-memory map lives in a
@@ -114,7 +151,11 @@ export function readLedgerFile(path) {
114
151
  if (typeof e.requirementId !== 'string' || !DISPOSITIONS.includes(e.disposition))
115
152
  continue;
116
153
  const prior = merged.get(e.requirementId);
117
- if (prior === undefined || rank[e.disposition] < rank[prior.disposition])
154
+ // `<=`, not `<`: a per-`it` FOLD (2.37.0) appends the cumulative row after
155
+ // the leg rows it folded, so on an equal disposition the LAST line is the
156
+ // one carrying the summed `assertionCount`. For genuinely duplicate lines
157
+ // the two are identical and the choice is a no-op.
158
+ if (prior === undefined || rank[e.disposition] <= rank[prior.disposition])
118
159
  merged.set(e.requirementId, e);
119
160
  }
120
161
  return [...merged.values()].sort((a, b) => a.requirementId.localeCompare(b.requirementId));
@@ -110,9 +110,13 @@ export function resolveItRecord(state, assertionCalls, gate, noted, firstError,
110
110
  * closed), none an optional extra. Scoped to major 2 because the 146 v1-side
111
111
  * sites were not measured and v1 bundles are read through its EOS.
112
112
  */
113
- blockedStands = false) {
114
- if (state === 'fail')
115
- return { disposition: 'executed-fail', detail: `the test executed and failed: ${(firstError ?? 'no message').slice(0, 300)}` };
113
+ blockedStands = false,
114
+ /** The `it` title, so a failed row says WHICH leg of the requirement failed. */
115
+ testName) {
116
+ if (state === 'fail') {
117
+ const where = testName === undefined || testName.trim() === '' ? '' : ` in "${testName.slice(0, 120)}"`;
118
+ return { disposition: 'executed-fail', detail: `the test executed and failed${where}: ${(firstError ?? 'no message').slice(0, 300)}` };
119
+ }
116
120
  if (state === 'pass' && assertionCalls > 0) {
117
121
  // `blockedDespiteAssertions` (soft-skip.ts): the leg says its setup
118
122
  // assertions are not the requirement, and the requirement went unobserved.
@@ -138,11 +142,34 @@ blockedStands = false) {
138
142
  return { disposition: 'blocked', detail: 'unclassified return: the test passed with zero assertions and recorded no reason — RFC 0148 §A resolves it to blocked, never to a pass' };
139
143
  return { disposition: 'skipped', detail: 'vitest skipped the test (ctx.skip / it.skip) without a recorded gate reason' };
140
144
  }
145
+ /**
146
+ * The `executed-fail` detail for a file row: WHICH cases failed, and what the
147
+ * first one said.
148
+ *
149
+ * Until 2.37.0 this was the fixed string "one or more assertions in the file
150
+ * failed". A tier-2 host read exactly that for `v2-run-bulk-cancel` — 10
151
+ * assertions, no case name, no message, and nothing else in the record for that
152
+ * file — and had to hand-probe every assertion in the file against production to
153
+ * find out what had happened. A bundle row whose only detail is that sentence is
154
+ * undiagnosable by construction, and every future flicker in any scenario had
155
+ * the same problem.
156
+ */
157
+ export function failureDetail(failures, failedCount) {
158
+ if (failures.length === 0) {
159
+ return `${failedCount} test(s) in the file failed; the runner captured no message`;
160
+ }
161
+ const head = failures[0];
162
+ const named = failures.slice(0, 3).map((f) => `"${f.name.slice(0, 120)}"`).join(', ');
163
+ const more = failures.length > 3 ? ` (+${failures.length - 3} more)` : '';
164
+ const msg = head.message === undefined || head.message.trim() === '' ? 'no message' : head.message.slice(0, 400);
165
+ return `${failures.length} test(s) failed — ${named}${more}; first failure: ${msg}`;
166
+ }
141
167
  /** Worker half: fold a file's per-test states (+ any gate-recorded reason) into
142
168
  * the ONE disposition the file records. */
143
- export function fileDisposition(states, gateReason, assertionCount) {
144
- if (states.some((s) => s === 'fail'))
145
- return { disposition: 'executed-fail', detail: 'one or more assertions in the file failed' };
169
+ export function fileDisposition(states, gateReason, assertionCount, failures = []) {
170
+ const failed = states.filter((s) => s === 'fail').length;
171
+ if (failed > 0)
172
+ return { disposition: 'executed-fail', detail: failureDetail(failures, failed) };
146
173
  if (states.some((s) => s === 'pass')) {
147
174
  // A test that early-returned through `behaviorGate` is reported by vitest
148
175
  // as a pass with zero assertions. When EVERY passing test in the file did
@@ -182,7 +209,9 @@ export function fileDisposition(states, gateReason, assertionCount) {
182
209
  * - every test `ctx.skip()`ped ⇒ the file's noted reason if it wrote one
183
210
  * BEFORE skipping (`ctx.skip()` throws), else `blocked` + the marker.
184
211
  */
185
- export function resolveFileRecord(states, gateReason, assertionCount, noted, specCoherenceFile) {
212
+ export function resolveFileRecord(states, gateReason, assertionCount, noted, specCoherenceFile,
213
+ /** The failed cases, so an `executed-fail` row NAMES them (2.37.0). */
214
+ failures = []) {
186
215
  // A scenario whose subject is the CORPUS, skipped because the published
187
216
  // tarball does not bundle spec/v1/. RFC 0148 §A: `blocked` is defined over
188
217
  // ADVERTISED BEHAVIOUR, and there is none here — nothing about the host was
@@ -196,7 +225,7 @@ export function resolveFileRecord(states, gateReason, assertionCount, noted, spe
196
225
  && assertionCount === 0) {
197
226
  return { disposition: 'inapplicable', detail: SPEC_COHERENCE_DETAIL };
198
227
  }
199
- let { disposition, detail } = fileDisposition(states, gateReason, assertionCount);
228
+ let { disposition, detail } = fileDisposition(states, gateReason, assertionCount, failures);
200
229
  if (disposition === 'executed-pass' && assertionCount === 0) {
201
230
  if (noted !== null) {
202
231
  disposition = noted.kind;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.37.0",
4
- "stampSha256": "7122d36a173c88dfe75b1fd136824e9a3601583b9035916a666b49e11010b77e"
3
+ "version": "2.37.1",
4
+ "stampSha256": "3f4b0c233c7927e94f4ea521b3d97e8c502de481b6e5d17d5fd58463aff9ad79"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.37.0",
3
+ "version": "2.37.1",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,6 +56,6 @@
56
56
  "@openwop/spec-artifacts": "file:../spec-artifacts"
57
57
  },
58
58
  "peerDependencies": {
59
- "@openwop/spec-artifacts": "2.37.0"
59
+ "@openwop/spec-artifacts": "2.37.1"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "$comment": "GENERATED by conformance/scripts/generate-requirement-registry.mjs — do not edit. One record per it()/test() in src/scenarios. Ids: openwop.it.<file-stem>.<title-slug>[~n] (src/lib/requirement-ids.ts). A record with id null has an interpolated title; its run-time row is keyed by the rendered title and maps here by file+line only. Renamed ids need a row in requirement-aliases.json.",
3
3
  "generatedFrom": "src/scenarios/*.test.ts",
4
4
  "counts": {
5
- "files": 628,
6
- "tests": 2401,
7
- "withStableId": 2401,
5
+ "files": 629,
6
+ "tests": 2404,
7
+ "withStableId": 2404,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2291
9
+ "explicitIds": 2294
10
10
  },
11
11
  "records": [
12
12
  {
@@ -5956,7 +5956,7 @@
5956
5956
  {
5957
5957
  "id": "openwop.it.auth-subject-link.subjectlinking-true-is-only-claimed-alongside-both-saml-and-scim-profiles",
5958
5958
  "file": "auth-subject-link.test.ts",
5959
- "line": 61,
5959
+ "line": 62,
5960
5960
  "title": "subjectLinking:true is only claimed alongside both SAML and SCIM profiles",
5961
5961
  "explicitId": "openwop.it.auth-subject-link.subjectlinking-true-is-only-claimed-alongside-both-saml-and-scim-profiles",
5962
5962
  "citations": [
@@ -5969,7 +5969,7 @@
5969
5969
  {
5970
5970
  "id": "openwop.it.auth-subject-link.advertising-both-profiles-requires-subjectlinking-true-a-subjectlinkkey-rfc-0164",
5971
5971
  "file": "auth-subject-link.test.ts",
5972
- "line": 76,
5972
+ "line": 77,
5973
5973
  "title": "advertising BOTH profiles REQUIRES subjectLinking:true + a subjectLinkKey (RFC 0164 — the vulnerable shape fails, never inapplicable)",
5974
5974
  "explicitId": "openwop.it.auth-subject-link.advertising-both-profiles-requires-subjectlinking-true-a-subjectlinkkey-rfc-0164",
5975
5975
  "citations": [
@@ -5987,7 +5987,7 @@
5987
5987
  {
5988
5988
  "id": "openwop.it.auth-subject-link.a-scim-deactivation-fail-closes-the-linked-saml-identity",
5989
5989
  "file": "auth-subject-link.test.ts",
5990
- "line": 106,
5990
+ "line": 107,
5991
5991
  "title": "a SCIM deactivation fail-closes the linked SAML identity",
5992
5992
  "explicitId": "openwop.it.auth-subject-link.a-scim-deactivation-fail-closes-the-linked-saml-identity",
5993
5993
  "citations": [
@@ -6012,7 +6012,7 @@
6012
6012
  {
6013
6013
  "id": "openwop.it.auth-subject-link.a-mutable-pii-link-key-email-never-produces-a-cross-lane-pass",
6014
6014
  "file": "auth-subject-link.test.ts",
6015
- "line": 152,
6015
+ "line": 169,
6016
6016
  "title": "a mutable/PII link key (email) never produces a cross-lane pass",
6017
6017
  "explicitId": "openwop.it.auth-subject-link.a-mutable-pii-link-key-email-never-produces-a-cross-lane-pass",
6018
6018
  "citations": [
@@ -6029,7 +6029,7 @@
6029
6029
  {
6030
6030
  "id": "openwop.it.auth-subject-link.a-scim-deactivation-does-not-deny-a-saml-login-whose-run-subject-is-legacy-issue",
6031
6031
  "file": "auth-subject-link.test.ts",
6032
- "line": 202,
6032
+ "line": 219,
6033
6033
  "title": "a SCIM deactivation does not deny a SAML login whose run subject is legacy-issued",
6034
6034
  "explicitId": null,
6035
6035
  "citations": []
@@ -25733,7 +25733,7 @@
25733
25733
  {
25734
25734
  "id": "openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation",
25735
25735
  "file": "trigger-bridge-delivery.test.ts",
25736
- "line": 54,
25736
+ "line": 55,
25737
25737
  "title": "de-dups by dedupKey, retries to dead-letter, and links delivery→run causation",
25738
25738
  "explicitId": "openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation",
25739
25739
  "citations": [
@@ -27265,7 +27265,7 @@
27265
27265
  {
27266
27266
  "id": "openwop.it.v2-bound-id-kinds.subscriptionid-the-webhookid-a-host-emits-on-a-delivery-is-the-one-it-minted",
27267
27267
  "file": "v2-bound-id-kinds.test.ts",
27268
- "line": 132,
27268
+ "line": 140,
27269
27269
  "title": "subscriptionId: the webhookId a host EMITS on a delivery is the one it minted",
27270
27270
  "explicitId": "openwop.requirement.0187.bound-id-kinds.webhook-emitted",
27271
27271
  "citations": [
@@ -27284,7 +27284,7 @@
27284
27284
  {
27285
27285
  "id": "openwop.it.v2-bound-id-kinds.interruptid-the-id-a-suspension-puts-on-the-wire-is-bound",
27286
27286
  "file": "v2-bound-id-kinds.test.ts",
27287
- "line": 179,
27287
+ "line": 192,
27288
27288
  "title": "interruptId: the id a suspension puts on the wire is bound",
27289
27289
  "explicitId": null,
27290
27290
  "citations": []
@@ -27292,7 +27292,7 @@
27292
27292
  {
27293
27293
  "id": "openwop.it.v2-bound-id-kinds.effectid-every-row-get-runs-runid-effects-serves-is-bound",
27294
27294
  "file": "v2-bound-id-kinds.test.ts",
27295
- "line": 200,
27295
+ "line": 213,
27296
27296
  "title": "effectId: every row GET /runs/{runId}/effects serves is bound",
27297
27297
  "explicitId": "openwop.requirement.0187.bound-id-kinds.per-kind",
27298
27298
  "citations": [
@@ -27305,7 +27305,7 @@
27305
27305
  {
27306
27306
  "id": "openwop.it.v2-bound-id-kinds.deliveryid-every-record-in-the-dead-letter-read-carries-a-bound-id-in-the-caller",
27307
27307
  "file": "v2-bound-id-kinds.test.ts",
27308
- "line": 215,
27308
+ "line": 228,
27309
27309
  "title": "deliveryId: every record in the dead-letter read carries a bound id in the caller's tenant",
27310
27310
  "explicitId": "openwop.requirement.0187.bound-id-kinds.per-kind",
27311
27311
  "citations": [
@@ -28883,10 +28883,10 @@
28883
28883
  ]
28884
28884
  },
28885
28885
  {
28886
- "id": "openwop.it.v2-idempotency-in-flight.concurrent-same-key-creates-yield-one-run-each-loser-is-a-marked-replay-of-a-fin",
28886
+ "id": "openwop.it.v2-idempotency-in-flight.concurrent-same-key-creates-yield-exactly-one-run",
28887
28887
  "file": "v2-idempotency-in-flight.test.ts",
28888
- "line": 47,
28889
- "title": "concurrent same-key creates yield one run; each loser is a marked replay of a final outcome or 409 idempotency_in_flight with no retry timing in details",
28888
+ "line": 101,
28889
+ "title": "concurrent same-key creates yield exactly one run",
28890
28890
  "explicitId": "openwop.requirement.0213.in-flight-one-winner",
28891
28891
  "citations": [
28892
28892
  {
@@ -28898,7 +28898,16 @@
28898
28898
  "section": "spec/v2/core/idempotency.md Concurrency",
28899
28899
  "requirement": null,
28900
28900
  "interpolated": true
28901
- },
28901
+ }
28902
+ ]
28903
+ },
28904
+ {
28905
+ "id": "openwop.it.v2-idempotency-in-flight.each-loser-is-a-marked-replay-of-a-final-outcome-or-409-idempotency-in-flight-wi",
28906
+ "file": "v2-idempotency-in-flight.test.ts",
28907
+ "line": 109,
28908
+ "title": "each loser is a marked replay of a final outcome, or 409 idempotency_in_flight with no retry timing in details",
28909
+ "explicitId": "openwop.requirement.0213.in-flight-loser-outcome",
28910
+ "citations": [
28902
28911
  {
28903
28912
  "section": "spec/v2/core/idempotency.md Concurrency",
28904
28913
  "requirement": null,
@@ -32718,7 +32727,7 @@
32718
32727
  {
32719
32728
  "id": "openwop.it.v2-webhook-delivery-shape.a-major-2-subscriber-receives-the-v2-rendering-the-delivery-validates-and-run-st",
32720
32729
  "file": "v2-webhook-delivery-shape.test.ts",
32721
- "line": 220,
32730
+ "line": 217,
32722
32731
  "title": "a major-2 subscriber receives the v2 rendering: the delivery validates, and run.started.owner carries subject, never principal",
32723
32732
  "explicitId": "openwop.requirement.0171.webhook-delivery-shape",
32724
32733
  "citations": [
@@ -32749,7 +32758,7 @@
32749
32758
  {
32750
32759
  "id": "openwop.it.v2-webhook-delivery-shape.a-major-1-subscriber-still-receives-the-v1-rendering-the-v1-wire-does-not-move-m",
32751
32760
  "file": "v2-webhook-delivery-shape.test.ts",
32752
- "line": 240,
32761
+ "line": 237,
32753
32762
  "title": "a major-1 subscriber still receives the v1 rendering — the v1 wire does not move mid-overlap",
32754
32763
  "explicitId": "openwop.requirement.0171.webhook-delivery-shape",
32755
32764
  "citations": [
@@ -32767,7 +32776,7 @@
32767
32776
  {
32768
32777
  "id": "openwop.it.v2-webhook-delivery-shape.a-seeded-era-2-run-is-delivered-projected-too-the-fan-out-branch-a-fresh-run-can",
32769
32778
  "file": "v2-webhook-delivery-shape.test.ts",
32770
- "line": 264,
32779
+ "line": 261,
32771
32780
  "title": "a seeded era-2 run is delivered projected too — the fan-out branch a fresh run cannot reach (seam-gated)",
32772
32781
  "explicitId": "openwop.requirement.0171.webhook-delivery-shape",
32773
32782
  "citations": [
@@ -32785,7 +32794,7 @@
32785
32794
  {
32786
32795
  "id": "openwop.it.v2-webhook-durable-delivery.a-failed-attempt-is-retried-and-the-event-is-delivered-at-least-once",
32787
32796
  "file": "v2-webhook-durable-delivery.test.ts",
32788
- "line": 235,
32797
+ "line": 238,
32789
32798
  "title": "a failed attempt is retried and the event is delivered at least once",
32790
32799
  "explicitId": "openwop.requirement.0173.webhook-durable-delivery",
32791
32800
  "citations": [
@@ -32795,7 +32804,8 @@
32795
32804
  },
32796
32805
  {
32797
32806
  "section": "webhooks.md §Durability",
32798
- "requirement": "the host MUST attempt delivery of run.completed for THIS run to the registered subscriber"
32807
+ "requirement": null,
32808
+ "interpolated": true
32799
32809
  },
32800
32810
  {
32801
32811
  "section": "webhooks.md §Durability",
@@ -32816,7 +32826,7 @@
32816
32826
  {
32817
32827
  "id": "openwop.it.v2-webhook-durable-delivery.an-exhausted-delivery-is-dead-lettered-never-dropped",
32818
32828
  "file": "v2-webhook-durable-delivery.test.ts",
32819
- "line": 323,
32829
+ "line": 339,
32820
32830
  "title": "an exhausted delivery is dead-lettered, never dropped",
32821
32831
  "explicitId": "openwop.requirement.0173.webhook-durable-delivery.dead-letter",
32822
32832
  "citations": [
@@ -32850,7 +32860,7 @@
32850
32860
  {
32851
32861
  "id": "openwop.it.v2-webhook-durable-delivery.the-dead-letter-read-is-served-and-its-records-carry-no-payload",
32852
32862
  "file": "v2-webhook-durable-delivery.test.ts",
32853
- "line": 417,
32863
+ "line": 433,
32854
32864
  "title": "the dead-letter read is served and its records carry no payload",
32855
32865
  "explicitId": "openwop.requirement.0188.dead-letter-read",
32856
32866
  "citations": [
@@ -32863,7 +32873,7 @@
32863
32873
  {
32864
32874
  "id": "openwop.it.v2-webhook-durable-delivery.a-dead-letter-record-carries-no-delivered-payload",
32865
32875
  "file": "v2-webhook-durable-delivery.test.ts",
32866
- "line": 438,
32876
+ "line": 454,
32867
32877
  "title": "a dead-letter record carries no delivered payload",
32868
32878
  "explicitId": "openwop.requirement.0188.dead-letter-content-free",
32869
32879
  "citations": [
@@ -33990,7 +34000,7 @@
33990
34000
  {
33991
34001
  "id": "openwop.it.webhook-signed-delivery.host-posts-run-events-to-subscriber-with-valid-x-openwop-signature",
33992
34002
  "file": "webhook-signed-delivery.test.ts",
33993
- "line": 169,
34003
+ "line": 168,
33994
34004
  "title": "host POSTs run events to subscriber with valid X-openwop-Signature",
33995
34005
  "explicitId": "openwop.it.webhook-signed-delivery.host-posts-run-events-to-subscriber-with-valid-x-openwop-signature",
33996
34006
  "citations": [
@@ -34041,7 +34051,7 @@
34041
34051
  {
34042
34052
  "id": "openwop.it.webhook-signed-delivery.control-with-a-tunnel-wired-the-host-still-refuses-the-loopback-receiver",
34043
34053
  "file": "webhook-signed-delivery.test.ts",
34044
- "line": 410,
34054
+ "line": 423,
34045
34055
  "title": "control: with a tunnel wired, the host still refuses the loopback receiver",
34046
34056
  "explicitId": "openwop.it.webhook-signed-delivery.control-with-a-tunnel-wired-the-host-still-refuses-the-loopback-receiver",
34047
34057
  "citations": [
@@ -40412,6 +40422,48 @@
40412
40422
  }
40413
40423
  ]
40414
40424
  },
40425
+ {
40426
+ "id": "openwop.it.v1-webhook-unregister-contract.the-tenantid-the-v1-prose-requires-on-unregister-is-a-required-query-parameter-o",
40427
+ "file": "v1-webhook-unregister-contract.test.ts",
40428
+ "line": 68,
40429
+ "title": "the tenantId the v1 prose requires on unregister is a required query parameter of api/openapi.yaml unregisterWebhook",
40430
+ "explicitId": "openwop.requirement.webhooks.v1-unregister-tenantid-in-contract",
40431
+ "citations": [
40432
+ {
40433
+ "section": "spec/v1/webhooks.md §Unregister",
40434
+ "requirement": "spec/v1/webhooks.md §Unregister MUST still require the tenantId query parameter — this leg is only meaningful while it does"
40435
+ },
40436
+ {
40437
+ "section": "spec/v1/webhooks.md §Unregister",
40438
+ "requirement": "api/openapi.yaml MUST define DELETE /v1/webhooks/{webhookId}"
40439
+ },
40440
+ {
40441
+ "section": "spec/v1/webhooks.md §Unregister",
40442
+ "requirement": "the v1 operation is unregisterWebhook"
40443
+ },
40444
+ {
40445
+ "section": "spec/v1/webhooks.md §Unregister",
40446
+ "requirement": "unregisterWebhook MUST declare `tenantId` as a REQUIRED query parameter — the prose requires it, and a client generated from a contract that omits it cannot issue a conforming request"
40447
+ }
40448
+ ]
40449
+ },
40450
+ {
40451
+ "id": "openwop.it.v1-webhook-unregister-contract.the-v2-twin-carries-the-tenant-in-its-id-and-does-not-grow-the-v1-query-paramete",
40452
+ "file": "v1-webhook-unregister-contract.test.ts",
40453
+ "line": 87,
40454
+ "title": "the v2 twin carries the tenant in its id and does not grow the v1 query parameter",
40455
+ "explicitId": "openwop.requirement.webhooks.v2-unregister-no-tenantid-query",
40456
+ "citations": [
40457
+ {
40458
+ "section": "api/v2/openapi.yaml unregisterWebhook",
40459
+ "requirement": "api/v2/openapi.yaml MUST define DELETE /webhooks/{webhookId}"
40460
+ },
40461
+ {
40462
+ "section": "identity.md §5 (RFC 0187 §A.1)",
40463
+ "requirement": "the v2 unregisterWebhook MUST NOT declare a tenantId parameter — v2 carries the tenant inside the tenant-bound webhookId, and a second tenant source could disagree with it"
40464
+ }
40465
+ ]
40466
+ },
40415
40467
  {
40416
40468
  "id": "openwop.it.v2-alias-covers-reword.the-corpus-gate-script-exits-0",
40417
40469
  "file": "v2-alias-covers-reword.test.ts",
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
3
3
  "package": "@openwop/spec-artifacts",
4
- "version": "2.37.0",
5
- "corpusTag": "v2.37.0",
4
+ "version": "2.37.1",
5
+ "corpusTag": "v2.37.1",
6
6
  "files": {
7
7
  "api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
8
8
  "api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
9
9
  "api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
10
- "api/openapi.yaml": "df108919a368e0a593f8f3dc8e2150a07d394e8971e1f91a3d6301e2e19cb132",
10
+ "api/openapi.yaml": "6366b5514be71098e9ea41fc81ff52bc39db1a547b353c0b344cc2a2bf5370aa",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "e60b9ff834b83ebe710a6960a964296186c529b67453c11c263a78dc83e76fe3",
13
- "api/v2/asyncapi.yaml": "3f584bb004c0273a20cadf8a3a2fdfd219d1851734faec5c99b0d69022ada529",
14
- "api/v2/openapi.yaml": "a0880db46930a58b99c860fe04f17b6b782d31a4cb4f86398446adbf36c062fe",
12
+ "api/seams-v2.yaml": "02711723089859b95144d788c0caabf215305570c7d48be1da5d2a5e21eff4e1",
13
+ "api/v2/asyncapi.yaml": "d492342046d1ec3a2dc43a28c4cf6d821d6319c30a8552cbc8fcd13806f7b2df",
14
+ "api/v2/openapi.yaml": "ba67b61f0e716444308ef7bc00416b8eee5829c28b67c5070358d35c3ca64d36",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -117,7 +117,7 @@
117
117
  "schemas/v2/audit-verify-result.schema.json": "a748dfadb5155adfde8a91045e76b0db6695005fe559035d71670fdd55edb3ba",
118
118
  "schemas/v2/budget-policy.schema.json": "c7449daeb6e1d95e7a047b8c2814d3d54748697c27046972834874867d9b5f4c",
119
119
  "schemas/v2/capabilities.schema.json": "79ddbbda4686fe66ec5b0aaf9bafb2385e2f8ccbb1ee39087d75cef9fd1f1628",
120
- "schemas/v2/certification-bundle.schema.json": "168a1e79097a755f6da57e4671586f4f2ddfaba26cb4ec2d5de3774517f4ac64",
120
+ "schemas/v2/certification-bundle.schema.json": "cb8ddf29a008b1c2e731fd61f855bf072790b700e458c702c40434b6fd47ba73",
121
121
  "schemas/v2/channel-presence-payload.schema.json": "4eeb224910e6f7eee3ab5e7c3899559c25c83a26d0022e0ad195c4672611977a",
122
122
  "schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
123
123
  "schemas/v2/chat-card-pack-manifest.schema.json": "f7cf09b30d3e1d2251620d84ea9f65541438d015ab44aec10d1dcfe68d8bd496",
@@ -204,7 +204,7 @@
204
204
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
205
205
  "spec/v1/alias-detectors.json": "2401fcb1c18cdd688c018b3d716ae6bca85c5e356220ee2b793bd9d81872412d",
206
206
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
207
- "spec/v1/core-standard-manifest.json": "d624b312258b0299906ed0ace1d6d79e6df4a0a2e5543d55aef06ff81e434da1",
207
+ "spec/v1/core-standard-manifest.json": "c92119aa65b12b7f670cc59e7768177d20f8f47173e7fbd15c3b0a6ef340810a",
208
208
  "spec/v1/deprecations.json": "2d03f4729810280147ea08c630ea65434f2dc5370567d0aac41c3595f8337d40",
209
209
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
210
210
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -292,9 +292,9 @@
292
292
  "spec/v2/path-manifest.json": "ae9b56d03a701061fd48ad0b73cb0373493f6a4547da92763b422295b5e19535",
293
293
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
294
294
  "spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
295
- "spec/v2/release.json": "e6cdd54b2e2b03cc6f8ccb56c977ba94188b66d9cb0a130053429eec219f95b5",
295
+ "spec/v2/release.json": "9422440ed08e9d8ff3e8190c3e91e862025fa8b5b49d468a536806f5a841ac13",
296
296
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
297
- "spec/v2/surface-baseline.json": "c07ff8163c6aadbd8673669ce07aa798ad4c8f99d59e8c33eaf5171f17777b10"
297
+ "spec/v2/surface-baseline.json": "13cfe5c31c3727a2e36980aa3e5a42f8ce1474b35beb99c5b71f487db006e71a"
298
298
  },
299
- "corpusCommit": "9b84226d4b9d3e81426065472016a5ef5deee933"
299
+ "corpusCommit": "0d370019eaaa841491be58d196e6d7a6934c71f0"
300
300
  }
@@ -36,7 +36,14 @@ import { resolvePublicFront } from './webhook-receiver.js';
36
36
 
37
37
  type Handler = (req: IncomingMessage, res: ServerResponse) => void;
38
38
 
39
- const PREFIX = '/fx/';
39
+ /**
40
+ * The path prefix a fronted nonce lives under. Exported so a caller that mints
41
+ * its OWN destination (`scoped-receiver.ts`, which needs the nonce
42
+ * unconditionally rather than only when it loses the pinned port) spells the
43
+ * path the same way `routeFronted` parses it.
44
+ */
45
+ export const FRONT_MUX_PREFIX = '/fx/';
46
+ const PREFIX = FRONT_MUX_PREFIX;
40
47
  const registry = new Map<string, Map<string, Handler>>();
41
48
 
42
49
  function pinnedPort(portEnv: string): number {