@openwop/openwop-conformance 2.24.0 → 2.26.0

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,14 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.26.0] — 2026-09-19 — RFC 0188 Accepted
4
+
5
+ - No scenario change. Suite pinned to corpus `2.26.0`; the reference-host bundle it certified is checked in at suite 2.25.0.
6
+
7
+ ## [2.25.0] — 2026-09-19 — a leg that asserted nothing
8
+
9
+ - `v2-webhook-durable-delivery` — the `0188.dead-letter-content-free` leg read a **fresh** subscription's sink, looped over zero rows, and asserted nothing; RFC 0148 §A resolves that silent return to `blocked`, denying certification. It now exhausts a real delivery first and asserts over the records it gets, and records `blocked` with a reason when the sink is empty.
10
+ - Suite pinned to corpus `2.25.0`.
11
+
3
12
  ## [2.24.0] — 2026-09-19 — the envelope-kind catalog gets a seat
4
13
 
5
14
  - `v2-envelope-catalog.test.ts` — six corpus witnesses for RFC 0193. The generator leg sabotages a scratch copy (removes a seat file, asserts the refusal names the family), because the committed tree is clean and a bare exit-0 wrapper would pass just as happily with the guard deleted. The optionality leg asserts no seat is `required` — MyndHyve publishes two of these records without one, and the v2 root is closed.
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.24.0 @openwop/spec-artifacts@2.24.0
14
+ npm install --legacy-peer-deps @openwop/openwop-conformance@2.26.0 @openwop/spec-artifacts@2.26.0
15
15
  # or run without install:
16
16
  npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
17
17
  ```
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.24.0",
4
- "stampSha256": "78d9e4c20c6b3cf16bf1605637d2a509a2f809ecb9e852d4f8588be61a6aa79d"
3
+ "version": "2.26.0",
4
+ "stampSha256": "e397c78d425a2525b04a13b490a5b86699e41f9553397e7e7f92f5caa9b2ba24"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.24.0",
3
+ "version": "2.26.0",
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.24.0"
59
+ "@openwop/spec-artifacts": "2.26.0"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -29469,7 +29469,8 @@
29469
29469
  "citations": [
29470
29470
  {
29471
29471
  "section": "RFC 0188 §B.1",
29472
- "requirement": "a dead-letter record MUST NOT carry the delivered body, headers or subscription secret — the queue is precisely the traffic the subscriber never received"
29472
+ "requirement": null,
29473
+ "interpolated": true
29473
29474
  }
29474
29475
  ]
29475
29476
  },
@@ -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.24.0",
5
- "corpusTag": "v2.24.0",
4
+ "version": "2.26.0",
5
+ "corpusTag": "v2.26.0",
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
10
  "api/openapi.yaml": "39081c59fb696159806b0f2f9a42e7e9ff830d622fcf2ad4159b21357580a955",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "1853ee35be966c6d59c73a6b34137e1c359273ba8b779255374a325aa56a9e25",
13
- "api/v2/asyncapi.yaml": "b7ed239aa5f099e5f0e81c59f24f559fcec567121a47437fd6c5222fe2ed8ecb",
14
- "api/v2/openapi.yaml": "ff1fc919c452ffc740d67374884a215eaa97b52b682920314c92ae8ee4b46ee4",
12
+ "api/seams-v2.yaml": "e428e9987de0e143fc1bb69f5c03b8f907d10b12e4fd07d007d9e9da18d5633f",
13
+ "api/v2/asyncapi.yaml": "ef1f4731966838122e8efdfe21bc32197afa01863a7ac673f09fe0b960ea07e6",
14
+ "api/v2/openapi.yaml": "b48ce1f4078ca6e760f26a86497ee78922709149f83626507593ed2ff0c016c0",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
@@ -201,7 +201,7 @@
201
201
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
202
202
  "spec/v1/alias-detectors.json": "40069d5976eeb6ba1384a648e57e5cfd673db3fce36175115fb8293bee9664d4",
203
203
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
204
- "spec/v1/core-standard-manifest.json": "f43c314b049fb6ac72ab198ad67f654defae3021adcdcb2559fa9fca96f9d213",
204
+ "spec/v1/core-standard-manifest.json": "672ff5dffbf6f6405dd5c49b131da3b09f6d07224c03b666d14b67ecf0b96c38",
205
205
  "spec/v1/deprecations.json": "307083ce29c23fd406015951f99a30d78d6187ff061d38dc9732f62191b40f3f",
206
206
  "spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
207
207
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -279,8 +279,8 @@
279
279
  "spec/v2/path-manifest.json": "c123c9fd77dc1b3f9c2e9346ace80cdae007e6511138894956abda01e61ec1c6",
280
280
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
281
281
  "spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
282
- "spec/v2/release.json": "9439a6dad8f37d50e4634f5aec8d68766cb7a42d50ef3a89f21e79f738f1d4f9",
282
+ "spec/v2/release.json": "462c3eb32192b27b3f365566bb240cbd33cc0d5096ce7f07dc64a1d4e57be1e4",
283
283
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
284
284
  },
285
- "corpusCommit": "d524cb367f593f7615cb9e3e22e5f14737c28501"
285
+ "corpusCommit": "6f3e9efef34d53e5ddce596470d7bc13c515d19b"
286
286
  }
@@ -400,22 +400,57 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
400
400
  it('a dead-letter record carries no delivered payload', async () => {
401
401
  // Its own `it`: RFC 0168 §A.1 allows one explicit requirement id per it(),
402
402
  // and `check-req-only` enforces it.
403
- if (!(await v2Discovery())) return softSkip('blocked', 'v2 discovery unreachable');
403
+ //
404
+ // THIS LEG USED TO REGISTER A FRESH SUBSCRIPTION AND READ ITS SINK. A fresh
405
+ // subscription has no dead letters by construction, so the loop over
406
+ // `deliveries` ran zero times, the test asserted NOTHING, and RFC 0148 §A
407
+ // resolves a silent return to `blocked` — which denies certification
408
+ // (RFC 0168 §E.1). It went unnoticed because every host recorded
409
+ // `inapplicable` for want of the facet: the first host ever to advertise
410
+ // `webhooks.deadLetter` was refused certification by this leg on its first
411
+ // cut, with 232 other rows passing and zero failing.
412
+ //
413
+ // §B.1 is a claim about what a REAL record carries, so the leg has to make
414
+ // one: exhaust a delivery against a receiver that never succeeds — the same
415
+ // move the sibling 0173 dead-letter leg performs — and then read.
416
+ const doc = await discovery();
417
+ if (!doc) return softSkip('blocked', 'discovery unreachable');
404
418
  const fam = await gateFamily('webhooks');
405
- if (!fam?.['deadLetter']) return softSkip('inapplicable', 'host does not advertise the webhooks.deadLetter facet');
406
- const reg = await driver.post('/webhooks', { url: 'https://subscriber.invalid/hook', events: ['run.completed'] });
407
- if (reg.status !== 201) return softSkip('blocked', `POST /webhooks answered ${reg.status}`);
408
- const webhookId = (reg.json as { webhookId?: unknown } | null)?.webhookId;
409
- if (typeof webhookId !== 'string') return softSkip('blocked', 'the mint returned no webhookId');
410
- const sink = await driver.get(`/webhooks/${projectBoundId(webhookId)}/dead-letters`);
411
- await driver.delete(`/webhooks/${encodeURIComponent(webhookId)}`);
419
+ if (!fam) return softSkip('inapplicable', 'webhooks family not advertised');
420
+ if (!fam['deadLetter']) return softSkip('inapplicable', 'host does not advertise the webhooks.deadLetter facet — RFC 0188 §A.5 makes the read a 404 rather than an obligation');
421
+ if (!fixtureAdvertised(doc, FIXTURE)) return softSkip('inapplicable', `${FIXTURE} fixture not advertised — no delivery to exhaust`);
422
+
423
+ const receiver = await startReceiver(Number.POSITIVE_INFINITY); // never succeeds
424
+ active = receiver.server;
425
+ const sub = await register(receiver.url);
426
+ if (sub === null) return softSkip('blocked', 'registration refused (reason recorded above)');
427
+ const create = await driver.post('/runs', { workflowId: FIXTURE });
428
+ if (create.status !== 201) {
429
+ await driver.delete(`/webhooks/${encodeURIComponent(sub.webhookId)}`);
430
+ return softSkip('blocked', `POST /runs answered ${create.status} — no delivery to exhaust`);
431
+ }
432
+ const runId = (create.json as { runId: string }).runId;
433
+ await waitTerminal(runId, 10_000);
434
+ const policy = advertisedRetryPolicy(doc);
435
+ // Filter by SUBSCRIPTION as well as run, as the sibling leg does: a host
436
+ // that cannot reach loopback shares one tunnelled receiver URL across
437
+ // scenarios, so another scenario's attempts land in this budget otherwise.
438
+ const ours = () => receiver.attempts.filter((a) => a.runId === runId && a.webhookId === sub.webhookId);
439
+ await waitFor(() => ours().length >= (policy?.maxAttempts ?? 2), retryWaitMs(doc));
440
+ await new Promise((r) => setTimeout(r, 1_000));
441
+
442
+ const sink = await driver.get(`/webhooks/${projectBoundId(sub.webhookId)}/dead-letters`);
443
+ await driver.delete(`/webhooks/${encodeURIComponent(sub.webhookId)}`);
412
444
  if (sink.status !== 200) return softSkip('blocked', `the dead-letter read answered ${sink.status}`);
413
445
  const rows = ((sink.json as { deliveries?: Array<Record<string, unknown>> } | null)?.deliveries ?? []);
414
- for (const r of rows) {
415
- expect(
416
- r['body'] === undefined && r['headers'] === undefined && r['secret'] === undefined,
417
- req('openwop.requirement.0188.dead-letter-content-free', 'RFC 0188 §B.1', 'a dead-letter record MUST NOT carry the delivered body, headers or subscription secret — the queue is precisely the traffic the subscriber never received'),
418
- ).toBe(true);
446
+ // An empty sink is NOT a pass. Recording one as a pass is exactly the
447
+ // vacuous witness this leg used to produce; say so instead.
448
+ if (rows.length === 0) {
449
+ return softSkip('blocked', 'the exhausted delivery did not reach the sink inside the retry window — §B.1 is a claim about a real record and there is none here to read');
419
450
  }
451
+ expect(
452
+ rows.every((r) => r['body'] === undefined && r['headers'] === undefined && r['secret'] === undefined),
453
+ req('openwop.requirement.0188.dead-letter-content-free', 'RFC 0188 §B.1', `a dead-letter record MUST NOT carry the delivered body, headers or subscription secret — the queue is precisely the traffic the subscriber never received, so a record carrying any of it turns one read scope into a replay of that traffic for the whole retention window (read ${rows.length} record(s))`),
454
+ ).toBe(true);
420
455
  }, DEAD_LETTER_TEST_TIMEOUT_MS);
421
456
  });