@openwop/openwop-conformance 2.4.3 → 2.4.5
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 +27 -0
- package/dist/cli.js +19 -1
- package/dist/lib/scenario-disposition.js +13 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +7 -7
- package/schemas/CORPUS-STAMP.json +10 -10
- package/src/cli.ts +21 -1
- package/src/lib/scenario-disposition.ts +13 -1
- package/src/scenarios/v2-bound-id-kinds.test.ts +10 -2
- package/src/scenarios/v2-webhook-delivery-shape.test.ts +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.4.5] — 2026-09-18 — the strict flag that was not wired, and the soft-skip that counted as a pass
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`--require-behavior` was never parsed** — it hit the arg loop's silent
|
|
8
|
+
default arm, so only `OPENWOP_REQUIRE_BEHAVIOR=true` enabled strict mode. A
|
|
9
|
+
cut naming the flag measured the loose answer. The flag now sets the variable,
|
|
10
|
+
and an unknown `--flag` exits 2 rather than being ignored.
|
|
11
|
+
- **`resolveItRecord` dropped the `partial-witness:` note** that
|
|
12
|
+
`resolveFileRecord` computes, so a leg that asserted and then soft-skipped
|
|
13
|
+
recorded a bare `executed-pass` at the granularity the RFC acceptance gate
|
|
14
|
+
reads.
|
|
15
|
+
- **`v2-bound-id-kinds`** now routes its mint through `resolveRegistrationUrl`,
|
|
16
|
+
so `OPENWOP_WEBHOOK_RECEIVER_URL` works on the leg whose blocked note told
|
|
17
|
+
operators to set it.
|
|
18
|
+
- **`v2-webhook-delivery-shape`** now honours `OPENWOP_WEBHOOK_RECEIVER_PORT`
|
|
19
|
+
and is registered in the receiver guard's scenario list. Two major-2 files
|
|
20
|
+
pin that port, so the webhook lane requires `--max-workers 1`.
|
|
21
|
+
|
|
22
|
+
## [2.4.4] — 2026-09-18 — the corpus's first `Superseded` RFC
|
|
23
|
+
|
|
24
|
+
**Why a patch, and why the suite version moves at all.** No conformance code
|
|
25
|
+
changes. RFC 0173 reached `Accepted` and RFC 0035 became `Superseded` in the
|
|
26
|
+
same PR (RFC 0174 §A.1), which closed gap G1 and therefore moved
|
|
27
|
+
`spec/v1/gaps.json` — a packed artifact — so the corpus version and the pinned
|
|
28
|
+
`@openwop/spec-artifacts` peer move with it.
|
|
29
|
+
|
|
3
30
|
## [2.4.3] — 2026-09-18 — a check with nothing to complain about is not `blocked`
|
|
4
31
|
|
|
5
32
|
**Why a patch.** Two assertions change shape inside existing legs; no scenario
|
package/dist/cli.js
CHANGED
|
@@ -163,9 +163,24 @@ function parseArgs(argv) {
|
|
|
163
163
|
case '--max-workers':
|
|
164
164
|
maxWorkers = parseMaxWorkers(nextValue(), '--max-workers');
|
|
165
165
|
break;
|
|
166
|
+
// `--require-behavior` was PRESCRIBED as the certification setting by the
|
|
167
|
+
// v2 migration runbook and recorded in INTEROP-MATRIX rows, and the CLI
|
|
168
|
+
// never parsed it: it fell into the silent default arm below while only
|
|
169
|
+
// `OPENWOP_REQUIRE_BEHAVIOR=true` did anything. So a cut that named the
|
|
170
|
+
// flag measured the LOOSE answer and reported the strict one — the exact
|
|
171
|
+
// failure shape the runbook warns about, in the tool that measures it.
|
|
172
|
+
case '--require-behavior':
|
|
173
|
+
process.env['OPENWOP_REQUIRE_BEHAVIOR'] = 'true';
|
|
174
|
+
break;
|
|
166
175
|
default:
|
|
167
176
|
if (arg.startsWith('-')) {
|
|
168
|
-
//
|
|
177
|
+
// An unknown flag is now a HARD ERROR, not a silent pass-through. The
|
|
178
|
+
// old arm is why the miss above went unnoticed for the whole v2
|
|
179
|
+
// program: a typo or a removed flag looked identical to a working one.
|
|
180
|
+
process.stderr.write(`openwop-conformance: unknown flag ${arg}\n` +
|
|
181
|
+
'Run `openwop-conformance --help` for usage. (A flag the CLI does not know is refused rather than ignored: ' +
|
|
182
|
+
'a certification run MUST NOT silently measure something other than what its command line says.)\n');
|
|
183
|
+
process.exit(2);
|
|
169
184
|
}
|
|
170
185
|
}
|
|
171
186
|
}
|
|
@@ -245,6 +260,9 @@ Certification (RFC 0089):
|
|
|
245
260
|
to <out.json>. Requires --base-url (and --api-key as usual).
|
|
246
261
|
|
|
247
262
|
Other:
|
|
263
|
+
--require-behavior Strict mode: an ADVERTISED behaviour that cannot be
|
|
264
|
+
observed FAILS instead of soft-skipping (RFC 0148 §B).
|
|
265
|
+
Equivalent to OPENWOP_REQUIRE_BEHAVIOR=true.
|
|
248
266
|
--help, -h Show this message
|
|
249
267
|
|
|
250
268
|
Examples:
|
|
@@ -99,8 +99,19 @@ export const PARTIAL_WITNESS_PREFIX = 'partial-witness: ';
|
|
|
99
99
|
export function resolveItRecord(state, assertionCalls, gate, noted, firstError) {
|
|
100
100
|
if (state === 'fail')
|
|
101
101
|
return { disposition: 'executed-fail', detail: `the test executed and failed: ${(firstError ?? 'no message').slice(0, 300)}` };
|
|
102
|
-
if (state === 'pass' && assertionCalls > 0)
|
|
103
|
-
|
|
102
|
+
if (state === 'pass' && assertionCalls > 0) {
|
|
103
|
+
// A leg that asserted AND THEN soft-skipped is only a partial witness, and
|
|
104
|
+
// the file-level record has always said so (`resolveFileRecord` below).
|
|
105
|
+
// This `it`-level record dropped the note — and the `it`-level rows are the
|
|
106
|
+
// ones RFC 0174 §B.1 rule 4 reads for acceptance. Measured consequence:
|
|
107
|
+
// `openwop.requirement.0173.webhook-durable-delivery.dead-letter` ends in an
|
|
108
|
+
// UNCONDITIONAL `softSkip('blocked', 'no normative dead-letter read surface…')`
|
|
109
|
+
// on every host, and still recorded a bare `executed-pass` that satisfied
|
|
110
|
+
// RFC 0173 §B's dead-letter row on every bundle in the corpus.
|
|
111
|
+
return noted !== null
|
|
112
|
+
? { disposition: 'executed-pass', detail: `${PARTIAL_WITNESS_PREFIX}${noted.kind}: ${noted.reason}` }
|
|
113
|
+
: { disposition: 'executed-pass' };
|
|
114
|
+
}
|
|
104
115
|
if (gate !== undefined)
|
|
105
116
|
return { disposition: gate.disposition, detail: gate.detail ?? `${gate.disposition} (gate recorded no reason)` };
|
|
106
117
|
if (noted !== null)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.5",
|
|
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.4.
|
|
59
|
+
"@openwop/spec-artifacts": "2.4.5"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -26009,7 +26009,7 @@
|
|
|
26009
26009
|
{
|
|
26010
26010
|
"id": "openwop.it.v2-bound-id-kinds.subscriptionid-post-webhooks-mints-a-bound-webhookid-the-projected-segment-is-ac",
|
|
26011
26011
|
"file": "v2-bound-id-kinds.test.ts",
|
|
26012
|
-
"line":
|
|
26012
|
+
"line": 61,
|
|
26013
26013
|
"title": "subscriptionId: POST /webhooks mints a bound webhookId, the projected segment is accepted, and a foreign tenant segment is refused 403",
|
|
26014
26014
|
"explicitId": "openwop.requirement.0187.bound-id-kinds.webhook",
|
|
26015
26015
|
"citations": [
|
|
@@ -26036,7 +26036,7 @@
|
|
|
26036
26036
|
{
|
|
26037
26037
|
"id": "openwop.it.v2-bound-id-kinds.interruptid-the-id-a-suspension-puts-on-the-wire-is-bound",
|
|
26038
26038
|
"file": "v2-bound-id-kinds.test.ts",
|
|
26039
|
-
"line":
|
|
26039
|
+
"line": 95,
|
|
26040
26040
|
"title": "interruptId: the id a suspension puts on the wire is bound",
|
|
26041
26041
|
"explicitId": null,
|
|
26042
26042
|
"citations": []
|
|
@@ -26044,7 +26044,7 @@
|
|
|
26044
26044
|
{
|
|
26045
26045
|
"id": "openwop.it.v2-bound-id-kinds.effectid-every-row-get-runs-runid-effects-serves-is-bound",
|
|
26046
26046
|
"file": "v2-bound-id-kinds.test.ts",
|
|
26047
|
-
"line":
|
|
26047
|
+
"line": 116,
|
|
26048
26048
|
"title": "effectId: every row GET /runs/{runId}/effects serves is bound",
|
|
26049
26049
|
"explicitId": "openwop.requirement.0187.bound-id-kinds.per-kind",
|
|
26050
26050
|
"citations": [
|
|
@@ -26057,7 +26057,7 @@
|
|
|
26057
26057
|
{
|
|
26058
26058
|
"id": "openwop.it.v2-bound-id-kinds.deliveryid-the-kind-has-no-wire-surface-to-be-bound-on-a-corpus-gap-recorded",
|
|
26059
26059
|
"file": "v2-bound-id-kinds.test.ts",
|
|
26060
|
-
"line":
|
|
26060
|
+
"line": 131,
|
|
26061
26061
|
"title": "deliveryId: the kind has no wire surface to be bound on — a corpus gap, recorded",
|
|
26062
26062
|
"explicitId": null,
|
|
26063
26063
|
"citations": []
|
|
@@ -29321,7 +29321,7 @@
|
|
|
29321
29321
|
{
|
|
29322
29322
|
"id": "openwop.it.v2-webhook-delivery-shape.a-major-2-subscriber-receives-the-v2-rendering-the-delivery-validates-and-run-st",
|
|
29323
29323
|
"file": "v2-webhook-delivery-shape.test.ts",
|
|
29324
|
-
"line":
|
|
29324
|
+
"line": 186,
|
|
29325
29325
|
"title": "a major-2 subscriber receives the v2 rendering: the delivery validates, and run.started.owner carries subject, never principal",
|
|
29326
29326
|
"explicitId": "openwop.requirement.0171.webhook-delivery-shape",
|
|
29327
29327
|
"citations": [
|
|
@@ -29352,7 +29352,7 @@
|
|
|
29352
29352
|
{
|
|
29353
29353
|
"id": "openwop.it.v2-webhook-delivery-shape.a-major-1-subscriber-still-receives-the-v1-rendering-the-v1-wire-does-not-move-m",
|
|
29354
29354
|
"file": "v2-webhook-delivery-shape.test.ts",
|
|
29355
|
-
"line":
|
|
29355
|
+
"line": 205,
|
|
29356
29356
|
"title": "a major-1 subscriber still receives the v1 rendering — the v1 wire does not move mid-overlap",
|
|
29357
29357
|
"explicitId": "openwop.requirement.0171.webhook-delivery-shape",
|
|
29358
29358
|
"citations": [
|
|
@@ -29370,7 +29370,7 @@
|
|
|
29370
29370
|
{
|
|
29371
29371
|
"id": "openwop.it.v2-webhook-delivery-shape.a-seeded-era-2-run-is-delivered-projected-too-the-fan-out-branch-a-fresh-run-can",
|
|
29372
29372
|
"file": "v2-webhook-delivery-shape.test.ts",
|
|
29373
|
-
"line":
|
|
29373
|
+
"line": 228,
|
|
29374
29374
|
"title": "a seeded era-2 run is delivered projected too — the fan-out branch a fresh run cannot reach (seam-gated)",
|
|
29375
29375
|
"explicitId": "openwop.requirement.0171.webhook-delivery-shape",
|
|
29376
29376
|
"citations": [
|
|
@@ -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.4.
|
|
5
|
-
"corpusTag": "v2.4.
|
|
4
|
+
"version": "2.4.5",
|
|
5
|
+
"corpusTag": "v2.4.5",
|
|
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": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
12
|
+
"api/seams-v2.yaml": "c630eb49d587cb3453b956ad7a7877b7afc7ce5730928df27b33b317736482c4",
|
|
13
|
+
"api/v2/asyncapi.yaml": "ce6eac3ac2407ae8a56ea08d4b4cbeca5b3c5bf573beb7f716f0f07d97f2a752",
|
|
14
|
+
"api/v2/openapi.yaml": "1e9a68170a184d71103a4dc03400f7f321c390042c00f42b1f2d9ebae50ff7ac",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -200,13 +200,13 @@
|
|
|
200
200
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
201
201
|
"spec/v1/alias-detectors.json": "e90916db12d2297e2e560b2d54d2c4702d6cb3bef7179a575844687a7ec17262",
|
|
202
202
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
203
|
-
"spec/v1/core-standard-manifest.json": "
|
|
203
|
+
"spec/v1/core-standard-manifest.json": "7b47c64730229681956af2a0cf2c9b8fe7ac641e590cf87a157e90fe49cd6678",
|
|
204
204
|
"spec/v1/deprecations.json": "c81c8f71460936c8d489d8270477c7661cf4a5d7b7bad86c98899750b5656027",
|
|
205
205
|
"spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
|
|
206
206
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
207
207
|
"spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
|
|
208
208
|
"spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
|
|
209
|
-
"spec/v1/gaps.json": "
|
|
209
|
+
"spec/v1/gaps.json": "4cfbca9d11e666685c874a113e7e4b4056014ff67224046258bfdd8ff1fc75a5",
|
|
210
210
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
211
211
|
"spec/v1/migrations.json": "8839497d6a4830b53fa7867014a8336f0d725ef2346352e54fb580b7947897af",
|
|
212
212
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
"spec/v2/core/errors.md": "ad85fc830139e587f20deabf88297b91b1fb6ce31e80d5442624a650246ab16e",
|
|
220
220
|
"spec/v2/core/events.md": "f1bac16419740f3b722febb6f5caaabed8838e092cba3d080fe9184024049dcf",
|
|
221
221
|
"spec/v2/core/form-content-packs.md": "6ffaae85d4550be48dd53fb6e2959af8aceb35bd87c5343b722885084f197713",
|
|
222
|
-
"spec/v2/core/headers.md": "
|
|
222
|
+
"spec/v2/core/headers.md": "d79eb0babdd4a1a8689d40dd7f84c711c729092259b2afe859481a0902dd7342",
|
|
223
223
|
"spec/v2/core/idempotency.md": "b86c352d964aebad532727f314e26ed2177d49de382076fa64d3162e2f56118d",
|
|
224
224
|
"spec/v2/core/identity.md": "9d2b823eb899463807452fe5721dcf09881837c161be024175957db27870b243",
|
|
225
225
|
"spec/v2/core/interop.md": "d4c404e469e1d9d9be88bc2c939d777e7d7c1d0419b1251c18770f039838a401",
|
|
@@ -271,8 +271,8 @@
|
|
|
271
271
|
"spec/v2/path-manifest.json": "a6f4e652e866889e8bde815874062f62929aee95dc8e58e7152c56adee262163",
|
|
272
272
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
273
273
|
"spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
|
|
274
|
-
"spec/v2/release.json": "
|
|
274
|
+
"spec/v2/release.json": "a6818de7c8879dc510a539c0c25f7bd06f489578f7454921504551cd1129dfea",
|
|
275
275
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
|
|
276
276
|
},
|
|
277
|
-
"corpusCommit": "
|
|
277
|
+
"corpusCommit": "9e47426865890fef13e6c0759f428f0e8454ba83"
|
|
278
278
|
}
|
package/src/cli.ts
CHANGED
|
@@ -181,9 +181,26 @@ function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
|
181
181
|
case '--max-workers':
|
|
182
182
|
maxWorkers = parseMaxWorkers(nextValue(), '--max-workers');
|
|
183
183
|
break;
|
|
184
|
+
// `--require-behavior` was PRESCRIBED as the certification setting by the
|
|
185
|
+
// v2 migration runbook and recorded in INTEROP-MATRIX rows, and the CLI
|
|
186
|
+
// never parsed it: it fell into the silent default arm below while only
|
|
187
|
+
// `OPENWOP_REQUIRE_BEHAVIOR=true` did anything. So a cut that named the
|
|
188
|
+
// flag measured the LOOSE answer and reported the strict one — the exact
|
|
189
|
+
// failure shape the runbook warns about, in the tool that measures it.
|
|
190
|
+
case '--require-behavior':
|
|
191
|
+
process.env['OPENWOP_REQUIRE_BEHAVIOR'] = 'true';
|
|
192
|
+
break;
|
|
184
193
|
default:
|
|
185
194
|
if (arg.startsWith('-')) {
|
|
186
|
-
//
|
|
195
|
+
// An unknown flag is now a HARD ERROR, not a silent pass-through. The
|
|
196
|
+
// old arm is why the miss above went unnoticed for the whole v2
|
|
197
|
+
// program: a typo or a removed flag looked identical to a working one.
|
|
198
|
+
process.stderr.write(
|
|
199
|
+
`openwop-conformance: unknown flag ${arg}\n` +
|
|
200
|
+
'Run `openwop-conformance --help` for usage. (A flag the CLI does not know is refused rather than ignored: ' +
|
|
201
|
+
'a certification run MUST NOT silently measure something other than what its command line says.)\n',
|
|
202
|
+
);
|
|
203
|
+
process.exit(2);
|
|
187
204
|
}
|
|
188
205
|
}
|
|
189
206
|
}
|
|
@@ -266,6 +283,9 @@ Certification (RFC 0089):
|
|
|
266
283
|
to <out.json>. Requires --base-url (and --api-key as usual).
|
|
267
284
|
|
|
268
285
|
Other:
|
|
286
|
+
--require-behavior Strict mode: an ADVERTISED behaviour that cannot be
|
|
287
|
+
observed FAILS instead of soft-skipping (RFC 0148 §B).
|
|
288
|
+
Equivalent to OPENWOP_REQUIRE_BEHAVIOR=true.
|
|
269
289
|
--help, -h Show this message
|
|
270
290
|
|
|
271
291
|
Examples:
|
|
@@ -106,7 +106,19 @@ export function resolveItRecord(
|
|
|
106
106
|
firstError?: string,
|
|
107
107
|
): { disposition: Disposition; detail?: string } {
|
|
108
108
|
if (state === 'fail') return { disposition: 'executed-fail', detail: `the test executed and failed: ${(firstError ?? 'no message').slice(0, 300)}` };
|
|
109
|
-
if (state === 'pass' && assertionCalls > 0)
|
|
109
|
+
if (state === 'pass' && assertionCalls > 0) {
|
|
110
|
+
// A leg that asserted AND THEN soft-skipped is only a partial witness, and
|
|
111
|
+
// the file-level record has always said so (`resolveFileRecord` below).
|
|
112
|
+
// This `it`-level record dropped the note — and the `it`-level rows are the
|
|
113
|
+
// ones RFC 0174 §B.1 rule 4 reads for acceptance. Measured consequence:
|
|
114
|
+
// `openwop.requirement.0173.webhook-durable-delivery.dead-letter` ends in an
|
|
115
|
+
// UNCONDITIONAL `softSkip('blocked', 'no normative dead-letter read surface…')`
|
|
116
|
+
// on every host, and still recorded a bare `executed-pass` that satisfied
|
|
117
|
+
// RFC 0173 §B's dead-letter row on every bundle in the corpus.
|
|
118
|
+
return noted !== null
|
|
119
|
+
? { disposition: 'executed-pass', detail: `${PARTIAL_WITNESS_PREFIX}${noted.kind}: ${noted.reason}` }
|
|
120
|
+
: { disposition: 'executed-pass' };
|
|
121
|
+
}
|
|
110
122
|
if (gate !== undefined) return { disposition: gate.disposition, detail: gate.detail ?? `${gate.disposition} (gate recorded no reason)` };
|
|
111
123
|
if (noted !== null) return { disposition: noted.kind, detail: noted.reason };
|
|
112
124
|
if (state === 'pass') 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' };
|
|
@@ -31,6 +31,7 @@ import { driver, type OpenWOPResponse } from '../lib/driver.js';
|
|
|
31
31
|
import { v2Discovery, gateFamily } from '../lib/v2.js';
|
|
32
32
|
import { readErrorCode } from '../lib/error-envelope.js';
|
|
33
33
|
import { softSkip } from '../lib/soft-skip.js';
|
|
34
|
+
import { resolveRegistrationUrl } from '../lib/webhook-receiver.js';
|
|
34
35
|
import { req } from '../lib/requirement-ids.js';
|
|
35
36
|
import { projectBoundId, BOUND_ID } from '../lib/bound-id.js';
|
|
36
37
|
|
|
@@ -60,9 +61,16 @@ describe('v2 bound-id kinds (identity.md §5, RFC 0187 §A)', () => {
|
|
|
60
61
|
it('subscriptionId: POST /webhooks mints a bound webhookId, the projected segment is accepted, and a foreign tenant segment is refused 403', async () => {
|
|
61
62
|
if (!(await v2Discovery())) return softSkip('blocked', 'v2 discovery unreachable');
|
|
62
63
|
if (!(await gateFamily('webhooks'))) return softSkip('inapplicable', 'webhooks family not advertised — no subscription to mint (gate recorded under openwop.family.webhooks)');
|
|
63
|
-
|
|
64
|
+
// Route the mint through `resolveRegistrationUrl` so an operator who sets
|
|
65
|
+
// OPENWOP_WEBHOOK_RECEIVER_URL actually gets a reachable registration here.
|
|
66
|
+
// Before this, the blocked note TOLD them to set that variable and this file
|
|
67
|
+
// never read it — inoperative advice in a suite where one blocked row denies
|
|
68
|
+
// every claimed profile (RFC 0168 §E.1). The fallback stays a reserved
|
|
69
|
+
// `.invalid` host: this leg only needs the mint, never a delivery.
|
|
70
|
+
const registration = resolveRegistrationUrl('https://subscriber.invalid/hook');
|
|
71
|
+
const reg = await http(() => driver.post('/webhooks', { url: registration.url, events: ['run.completed'] }));
|
|
64
72
|
if (reg === null) return softSkip('blocked', 'POST /webhooks unreachable (fetch failed)');
|
|
65
|
-
if (reg.status === 400 && readErrorCode(reg.json) === 'webhook_url_rejected') return softSkip('blocked',
|
|
73
|
+
if (reg.status === 400 && readErrorCode(reg.json) === 'webhook_url_rejected') return softSkip('blocked', `host SSRF guard rejected the registration URL ${registration.url}${registration.tunnelled ? ' (from OPENWOP_WEBHOOK_RECEIVER_URL)' : ' — set OPENWOP_WEBHOOK_RECEIVER_URL to a public https receiver, which THIS leg now honours'}`);
|
|
66
74
|
expect(reg.status, req(WEBHOOK_ID, 'webhooks.md §Surfaces', 'POST /webhooks MUST answer 201 { webhookId }')).toBe(201);
|
|
67
75
|
const webhookId = (reg.json as { webhookId?: unknown } | null)?.webhookId;
|
|
68
76
|
expectBound(webhookId, 'webhookId', 'POST /webhooks 201', WEBHOOK_ID);
|
|
@@ -77,11 +77,22 @@ async function startReceiver(): Promise<{ server: Server; url: string; deliverie
|
|
|
77
77
|
res.writeHead(204); res.end();
|
|
78
78
|
});
|
|
79
79
|
});
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// Honour OPENWOP_WEBHOOK_RECEIVER_PORT like `v2-webhook-durable-delivery`
|
|
81
|
+
// does. Without it, a tunnelled run registers the tunnel URL here and the
|
|
82
|
+
// tunnel forwards to the PINNED port — held by the other receiver, which
|
|
83
|
+
// answers 500 by design — so this file's `deliveries` stays empty, its legs
|
|
84
|
+
// soft-skip, and (because `register()` already asserted) the rows resolve
|
|
85
|
+
// `executed-pass`. A wire-shape scenario that never opened a delivery body
|
|
86
|
+
// went green. Two major-2 files now want the same pinned port, so the
|
|
87
|
+
// webhook lane MUST run with `--max-workers 1`, which is already the
|
|
88
|
+
// certification setting.
|
|
89
|
+
const pinned = Number(process.env['OPENWOP_WEBHOOK_RECEIVER_PORT'] ?? '');
|
|
90
|
+
const bindPort = Number.isInteger(pinned) && pinned > 0 && pinned < 65536 ? pinned : 0;
|
|
91
|
+
const binding = receiverBinding();
|
|
92
|
+
await new Promise<void>((resolve) => server.listen(bindPort, binding.bind, () => resolve()));
|
|
82
93
|
const addr = server.address();
|
|
83
94
|
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
84
|
-
return { server, url: `http://${advertise}:${port}/hook`, deliveries };
|
|
95
|
+
return { server, url: `http://${binding.advertise}:${port}/hook`, deliveries };
|
|
85
96
|
}
|
|
86
97
|
|
|
87
98
|
/**
|