@openwop/openwop-conformance 1.138.1 → 1.140.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.
@@ -16,13 +16,59 @@
16
16
  * Capability-gated: skips when the host does not advertise
17
17
  * `capabilities.webhooks.supported = true`.
18
18
  *
19
- * Operator contract: hosts that implement a SSRF guard on
20
- * `POST /v1/webhooks` (rejecting loopback / RFC1918 / link-local
21
- * destinations to protect deployer infrastructure) MUST allow the test
22
- * receiver. The SQLite reference host bypasses the guard when the
23
- * `OPENWOP_WEBHOOK_ALLOW_PRIVATE=true` env var is set at boot. Test-only
24
- * hosts SHOULD provide an equivalent opt-in. When the host rejects with
25
- * `400 webhook_url_rejected`, this scenario skips with a warning.
19
+ * Operator contract THREE gates, not one (clarified 2026-08-25).
20
+ *
21
+ * The test receiver is `http://127.0.0.1:{port}/`, and `webhooks.md`
22
+ * forbids it three separate times. A host honoring
23
+ * `OPENWOP_WEBHOOK_ALLOW_PRIVATE=true` (or an equivalent opt-in) MUST
24
+ * relax ALL THREE for this scenario to be witnessable:
25
+ *
26
+ * 1. **Scheme.** §"SSRF protection" bullet 1 rejects non-`https://`
27
+ * protocols, and §"Register" says `url` MUST be `https://`. The
28
+ * receiver is plain `http`. This gate is the one the flag's
29
+ * description omitted until 2026-08-25, and it fires FIRST on a
30
+ * host that validates scheme before address.
31
+ * 2. **Registration-time address check.** §"SSRF protection": the
32
+ * server MUST validate subscription URLs at registration time and
33
+ * reject loopback / RFC1918 / link-local / ULA / metadata.
34
+ * 3. **Delivery-time re-resolution.** §"Delivery-time egress
35
+ * validation (RFC 0093)": the dispatcher MUST re-resolve at
36
+ * delivery time and validate every resolved address against the
37
+ * same ranges.
38
+ *
39
+ * Gates 2 and 3 are INDEPENDENT MUSTs at different layers, so "which
40
+ * layer must the opt-in reach" is not a matter of taste: a relaxation
41
+ * reaching only one layer cannot produce a witness. Delivery-only leaves
42
+ * registration returning `400 webhook_url_rejected` (observed on a tier-2
43
+ * host, 2026-08-25); registration-only leaves the dispatcher re-resolving
44
+ * `127.0.0.1` and refusing to connect. A host whose opt-in reaches one
45
+ * layer is not non-conformant — it simply cannot witness this scenario,
46
+ * and that is a property of the test posture, not of its webhook signing.
47
+ *
48
+ * What that costs is more than a checkmark, and it is worth stating plainly
49
+ * because the cost is invisible from inside such a host. This scenario is
50
+ * typically the ONLY automated oracle a host has on its delivery header
51
+ * NAMES: a host-local delivery test almost always compares a hand-written
52
+ * literal in the test to a hand-written literal in the implementation, which
53
+ * is a mirror — typo both and it stays green — while the HMAC assertion
54
+ * beside it recomputes from `node:crypto` and is real. So the names go
55
+ * unchecked exactly where the bytes are checked well. All three reference
56
+ * hosts emitted invented header names for 16 days (openwop-examples#22) and
57
+ * the runs that would have caught it were the ones this scenario could not
58
+ * reach. A host that runs it is not thereby careful; it is thereby measured.
59
+ *
60
+ * Relaxing gates 2 and 3 is test-only posture. See
61
+ * `SECURITY/threat-model-secret-leakage.md` §4.9 for why a
62
+ * registration-time relaxation is the more dangerous of the two: it
63
+ * writes a durable subscription row that survives the flag being turned
64
+ * back off.
65
+ *
66
+ * When the host rejects with `400 webhook_url_rejected`, this scenario
67
+ * records `blocked` (RFC 0148 §A) — NOT a pass. Under a plain
68
+ * `vitest run` the console still prints "1 passed", because that is
69
+ * vitest reporting test outcomes rather than conformance dispositions;
70
+ * the run-end disposition summary (`src/global-setup.ts`) is where the
71
+ * `blocked` becomes visible without `--certify`.
26
72
  *
27
73
  * @see spec/v1/webhooks.md §"Signature scheme"
28
74
  */
package/src/setup.ts CHANGED
@@ -278,7 +278,7 @@ afterAll(({}, suite) => {
278
278
  // the marker detail — never to a pass. Floors still REJECT that row, so the
279
279
  // honest bundle row and the pressure to say why both survive. The rule is
280
280
  // `resolveFileRecord` (pinned by conformance-execution-witness.test.ts).
281
- const { disposition, detail } = resolveFileRecord(states, gateReason, assertionCount, softSkipDisposition(file));
281
+ const { disposition, detail } = resolveFileRecord(states, gateReason, assertionCount, softSkipDisposition(file), file);
282
282
  const fileRequirementId = requirementIdForFile(file);
283
283
  // A scenario that classified ITSELF wins outright — including its `detail` and
284
284
  // its `assertionCount`.
package/vitest.config.ts CHANGED
@@ -17,5 +17,12 @@ export default defineConfig({
17
17
  // populates `lib/fixtures.ts` so `describe.skipIf(...)` predicates
18
18
  // see the cached set when scenarios register their tests.
19
19
  setupFiles: ['src/setup.ts'],
20
+ // RFC 0148 §A run-end disposition summary. `setup.ts` records an honest
21
+ // disposition per scenario file on EVERY run; before this, only
22
+ // `--certify` set OPENWOP_LEDGER_PATH, so on a plain `vitest run` the
23
+ // recording was computed and then discarded, leaving "N passed" as the
24
+ // sole artifact for rows the suite had classified `blocked`. This routes
25
+ // the ledger to a temp file and prints what did not witness.
26
+ globalSetup: ['src/global-setup.ts'],
20
27
  },
21
28
  });