@openwop/openwop-conformance 2.40.1 → 2.40.3

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,18 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.40.3] — 2026-09-26 — a cookie-borne lane can be revoke-witnessed, and a secret-named setting no longer corrupts the discovery digest
4
+
5
+ - **`v2-revocation-honored` presents a cookie-lane credential as a cookie.** When the mint seam answers `presentation: { kind: "cookie", name }`, the probe sends `Cookie: <name>=<credential>` instead of a bearer token. The seams contract's `mintLaneCredential` response gains that optional member. Bearer stays the default when `presentation` is absent. The cookie branch is unexercised until a host serves it; openwop-app's `session` lane is the first expected.
6
+ - **RFC 0215 gap G1 closes in the contract peer.** `@openwop/spec-artifacts`' `spec/v1/gaps.json` records MyndHyve's measurement (openwop#1595), and `spec/v2/core/webhooks.md`'s `Stable` banner cites RFC 0215 now that it is `Accepted`. No scenario change.
7
+ - **A setting that names a secret is not scrubbed as one.** `evidenceSecretsFromEnv` sweeps every `OPENWOP_*` value whose NAME mentions a key/token/secret/password, and `scrubEvidence` then rewrites that value everywhere in the bundle. openwop-app's RFC 0201 setting `OPENWOP_WEBHOOK_SECRET_ROTATION_OVERLAP_S=60` was swept, the "60" inside `discovery.sha256` was rewritten, the bundle failed `^[0-9a-f]{64}$`, and its deploy certify refused to ship. Values guessed from names are now swept only when credential-shaped (at least 8 characters and not a bare integer); a credential the emitter hands over explicitly is scrubbed whatever its shape. Sabotage: without the floor the new redaction leg fails.
8
+ - **Version moved ahead of publication.** `@openwop/openwop-conformance` and its exact-pinned peer `@openwop/spec-artifacts` move to `2.40.3` because `2.40.2` is tagged. Not tagged, not published.
9
+
10
+ ## [2.40.2] — 2026-09-26 — production bundles stop carrying harness-issuer blocked rows, and a colocated companion is marked
11
+
12
+ - **RFC 0216: `--as-colocated-companion`, and a companion witnesses only the harness-trust-anchor rows.** The flag marks a v3 bundle `host.deployment: "colocated-companion"`. It is refused without `--certify` and bundle version 3. `witnessDigest` takes the marker into the preimage when present (`{ rows, relaxations?, deployment }`), so a signed companion cannot shed it: `--verify` then refuses with `witness-digest`. New coherence scenario `v2-colocated-companion` (7 legs, one id each) drives `scripts/check-harness-trust-anchors.mjs` and `scripts/check-companion-pairing.mjs` on tmpdir fixtures signed by the suite's own `signBundleV3`. Five sabotages were each run red.
13
+ - **The harness OIDC issuer is an instrument a host claims, so an honest production bundle is no longer uncertifiable.** Before this, `v2-oidc-id-token-audience` (`0200.id-token-aud`) and `v2-lane-exp-only-bound` (the four `0210.exp-only-*` / `credential-lifetime-code` rows) recorded `blocked` whenever the host did not trust `OPENWOP_TEST_OIDC_ISSUER_URL`. A production host must never trust a test issuer, and one `blocked` row denies certification bundle-wide (RFC 0168 §E.1). Every production bundle advertising an `oidc` or `exp-only` lane was therefore uncertifiable (openwop-app ADR 0745, corpus defect 1). The rows are `seam-gated`, and RFC 0168 §C.1 already rules that an unclaimed suite instrument is `inapplicable`, with `blocked` reserved for a claimed instrument that does not answer. The new `lib/harness-issuer.ts` treats the harness as claimed when the gated lane's advertised `issuers[]` lists it. When the lane does not list it, the row is `inapplicable` and names the issuers the host does trust. When it does, the legs run as before (a failed serve or a refused control is still `blocked`). RFC 0200 and RFC 0210 carry dated correction notes, and `SECURITY/invariants.yaml` `lane-exp-only-lifetime-bounded` is updated. No MUST changes. The residual is stated in the RFC 0210 note: a production bundle does not witness these rows, just as it witnesses no seam-gated row. The witness comes from a cut whose lane lists the harness. **The issuer now has a bind knob:** it listens via `receiverBinding()`, which is loopback, or `0.0.0.0` under `OPENWOP_CONFORMANCE_HARNESS_HOST`, on 2.39.4's `issuerListenPort()`. It waits out `EADDRINUSE` while the other scenario holds the shared URL. Before this, it always bound `127.0.0.1`, so a host in a Linux container reaching it through `host-gateway` found nothing listening (ADR 0745 defect 2; 2.39.4 fixed the port half). **`requirements.json` resolves `const ID` declared inside a test body** (`generate-requirement-registry.mjs`). Before, only module-level constants resolved, so 14 real ids recorded `explicitId: null`: `0210.lane-rule-surfaces-agree` (ADR 0745 defect 4), three `0199.*`, six `0201.*` and four `0214.*`. Those rows were labelled by their title-derived id whenever the leg did not reach its assertion. ADR 0745 defect 5, the reused kid, was already fixed in 2.37.0: every instance mints `openwop-conformance-key-<instance>-<n>`, pinned by `oidc-issuer.test.ts`. New suite self-test `src/lib/harness-issuer.test.ts` covers the claim predicate and the port knob. It is sabotage-proved: dropping the `issuers[]` check makes it fail 2/4.
14
+ - **Version moved ahead of publication.** `@openwop/openwop-conformance` and its exact-pinned peer `@openwop/spec-artifacts` move to `2.40.2` because `2.40.1` is released, and `spec-artifacts` now carries a changed `spec/v2/core/oauth.md` (RFC 0199's text restored; editorial, `COMPATIBILITY.md` §3 entry of 2026-09-26). No scenario changes. Not tagged, not published.
15
+
3
16
  ## [2.40.1] — 2026-09-26 — three declared-unwitnessable rows are reached and say why
4
17
 
5
18
  - **Three Falsifiability rows declared non-executable now have a leg that records the reason** (RFC 0174 §B.1 rule 4, second branch: "the suite must reach the leg and record a reason"). Each RFC's table declares one of these rows unwitnessable, but no suite leg referenced it, so no bundle could account for it. Each of RFCs 0197, 0205 and 0209 was held from `Accepted` by that one row. No leg can record `executed-pass` without observing the requirement.
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.40.1 @openwop/spec-artifacts@2.40.1
14
+ npm install --legacy-peer-deps @openwop/openwop-conformance@2.40.3 @openwop/spec-artifacts@2.40.3
15
15
  # or run without install:
16
16
  npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
17
17
  ```
package/dist/cli.js CHANGED
@@ -70,6 +70,7 @@ function parseArgs(argv) {
70
70
  let signingKeyPath, signingKeyId, verifierKeyPath, verifierKeyId;
71
71
  let targetMajor;
72
72
  let maxWorkers = parseMaxWorkers(process.env.OPENWOP_MAX_WORKERS, 'OPENWOP_MAX_WORKERS');
73
+ let colocatedCompanion = false;
73
74
  for (let i = 0; i < argv.length; i++) {
74
75
  const arg = argv[i] ?? '';
75
76
  if (arg === '-h' || arg === '--help') {
@@ -180,6 +181,12 @@ function parseArgs(argv) {
180
181
  case '--host-key':
181
182
  verifyHostKeyPath = nextValue();
182
183
  break;
184
+ // RFC 0216 §B — marks the bundle as cut from a colocated companion (the
185
+ // served host's image beside the suite, trusting a suite-held anchor).
186
+ // The marker is inside witnessSha256, so a signed companion cannot shed it.
187
+ case '--as-colocated-companion':
188
+ colocatedCompanion = true;
189
+ break;
183
190
  case '--require-behavior':
184
191
  process.env['OPENWOP_REQUIRE_BEHAVIOR'] = 'true';
185
192
  break;
@@ -215,6 +222,7 @@ function parseArgs(argv) {
215
222
  maxWorkers,
216
223
  verifyPath,
217
224
  verifyHostKeyPath,
225
+ colocatedCompanion,
218
226
  };
219
227
  }
220
228
  /** Parse a `--max-workers` / `OPENWOP_MAX_WORKERS` value: a positive integer, else exit 2. */
@@ -261,6 +269,10 @@ Certification (RFC 0089):
261
269
  §C) records per-requirement DISPOSITIONS instead of pass/fail/skip
262
270
  file lists, so "we could not check" stops being indistinguishable
263
271
  from "checked and it holds". See the note it prints.
272
+ --as-colocated-companion v3 (RFC 0216): mark the bundle as cut from a colocated companion — the
273
+ served host's image run beside the suite so it can trust a suite-held
274
+ trust anchor. Signed (inside witnessSha256). A companion is acceptance
275
+ evidence only for spec/v2/harness-trust-anchors.json rows.
264
276
  --max-workers <n> Cap concurrently running scenario files (vitest --maxWorkers).
265
277
  Default: one worker per CPU. Use a small number against a
266
278
  rate-limited production origin so 429s don't read as failures.
@@ -680,7 +692,7 @@ async function runCertify(args, baseUrl, apiKey) {
680
692
  bundleVersion: '3',
681
693
  generatedAt: new Date().toISOString(),
682
694
  suite: { name: '@openwop/openwop-conformance', version, targetMajor: target.major, specArtifactsVersion: lock?.version ?? 'repo-layout', ...(lock ? { stampSha256: lock.stampSha256 } : {}) },
683
- host: { name: host.name, version: host.version, ...(host.vendor ? { vendor: host.vendor } : {}), build, signingKeyId: keyId, ...(relaxations && relaxations.length ? { relaxations } : {}) },
695
+ host: { name: host.name, version: host.version, ...(host.vendor ? { vendor: host.vendor } : {}), build, signingKeyId: keyId, ...(relaxations && relaxations.length ? { relaxations } : {}), ...(args.colocatedCompanion ? { deployment: 'colocated-companion' } : {}) },
684
696
  // `document` is what makes `claimedProfiles[].certified` checkable by
685
697
  // someone other than this process (RFC 0148 §B(1)); v2 carried it and v3
686
698
  // dropped it. `sha256` is a digest of `canonicalJSON(document)`, so the
@@ -688,7 +700,7 @@ async function runCertify(args, baseUrl, apiKey) {
688
700
  discovery: { url: discoveryUrl, sha256, protocolVersions, preferredVersion, document },
689
701
  claimedProfiles: claimed3,
690
702
  results: { totals: totals3, requirements: rows3 },
691
- witnessSha256: witnessDigest(rows3, relaxations),
703
+ witnessSha256: witnessDigest(rows3, relaxations, args.colocatedCompanion ? 'colocated-companion' : undefined),
692
704
  assertionCount: rows3.reduce((n, r) => n + (r.assertions ?? 0), 0),
693
705
  ...(nonPass.length ? { detail: { nonPass: nonPass.map((r) => ({ id: r.id, result: r.result, reason: r.detail ?? '' })) } } : {}),
694
706
  // RFC 0158 §D: claimed ONLY when these rows support it. The verifier
@@ -856,6 +868,10 @@ async function runCertify(args, baseUrl, apiKey) {
856
868
  }
857
869
  async function main() {
858
870
  const args = parseArgs(process.argv.slice(2));
871
+ if (args.colocatedCompanion && (!args.certify || args.bundleVersion !== '3')) {
872
+ process.stderr.write('openwop-conformance: --as-colocated-companion marks a v3 certification bundle; it needs --certify with bundle version 3 (RFC 0216 §B)\n');
873
+ process.exit(2);
874
+ }
859
875
  // Refused before anything runs: a pinned-port certification that is not
860
876
  // single-worker loses the host's traffic to a worker nobody reads (lib/pinned-ports.ts).
861
877
  const pinnedConflict = pinnedPortWorkerConflict(process.env, args.maxWorkers, args.certify !== undefined);
@@ -49,12 +49,20 @@ export { canonicalJSON } from './jcs.js';
49
49
  * from the same bundle anywhere else (Czech sorts `ch` after `h`). Every
50
50
  * committed bundle's ids are `[a-z0-9.-]`, where the two orders coincide, so no
51
51
  * stored digest changes (v2-bundle-witness-preimage.test.ts pins that).
52
+ *
53
+ * RFC 0216 §B.7 — a colocated companion's `host.deployment` joins the preimage
54
+ * the same way, ONLY WHEN PRESENT (`{ rows, relaxations?, deployment }`), so a
55
+ * signed companion cannot shed the marker and pass as a served-host bundle, and
56
+ * every bundle without it digests exactly as before.
52
57
  */
53
- export function witnessDigest(rows, relaxations) {
58
+ export function witnessDigest(rows, relaxations, deployment) {
54
59
  const canonicalRows = [...rows].sort((a, b) => codeUnitCompare(a.id, b.id)).map((r) => ({ id: r.id, scenario: r.scenario, result: r.result, ...(r.assertions === undefined ? {} : { assertions: r.assertions }), ...(r.detail === undefined ? {} : { detail: r.detail }),
55
60
  // ONLY WHEN PRESENT: every bundle cut before 2.34.0 has no `evidence` and digests byte-identically.
56
61
  ...(r.evidence === undefined ? {} : { evidence: r.evidence }) }));
57
- const preimage = relaxations !== undefined && relaxations.length > 0 ? { rows: canonicalRows, relaxations } : canonicalRows;
62
+ const relaxed = relaxations !== undefined && relaxations.length > 0;
63
+ const preimage = relaxed || deployment !== undefined
64
+ ? { rows: canonicalRows, ...(relaxed ? { relaxations } : {}), ...(deployment !== undefined ? { deployment } : {}) }
65
+ : canonicalRows;
58
66
  return createHash('sha256').update(canonicalJSON(preimage), 'utf8').digest('hex');
59
67
  }
60
68
  /** The bytes the attestation covers. */
@@ -140,9 +148,9 @@ export function verifyBundleV3(bundle, opts = {}) {
140
148
  for (const k of Object.keys(expected))
141
149
  if (bundle.results?.totals?.[k] !== expected[k])
142
150
  rejections.push({ kind: 'totals-mismatch', detail: `totals.${k} is ${String(bundle.results?.totals?.[k])} but the rows count ${expected[k]}` });
143
- const digest = refusedAs(rejections, 'witness-digest', 'the rows or declared relaxations', () => witnessDigest(rows, bundle.host?.relaxations));
151
+ const digest = refusedAs(rejections, 'witness-digest', 'the rows or declared relaxations', () => witnessDigest(rows, bundle.host?.relaxations, bundle.host?.deployment));
144
152
  if (digest !== undefined && bundle.witnessSha256 !== digest)
145
- rejections.push({ kind: 'witness-digest', detail: `witnessSha256 ${String(bundle.witnessSha256).slice(0, 12)} does not equal the digest of the rows and declared relaxations (${digest.slice(0, 12)})` });
153
+ rejections.push({ kind: 'witness-digest', detail: `witnessSha256 ${String(bundle.witnessSha256).slice(0, 12)} does not equal the digest of the rows, declared relaxations and deployment marker (${digest.slice(0, 12)})` });
146
154
  const assertions = rows.reduce((n, r) => n + (r.assertions ?? 0), 0);
147
155
  if (bundle.assertionCount !== assertions)
148
156
  rejections.push({ kind: 'assertion-count', detail: `assertionCount is ${String(bundle.assertionCount)} but the rows sum to ${assertions}` });
@@ -267,6 +267,25 @@ export function scrubEvidence(value, secrets) {
267
267
  * are excluded, and a caller may name values that MUST stay visible (`except`)
268
268
  * regardless of which variable carried them.
269
269
  */
270
+ /**
271
+ * Whether an environment VALUE can be a credential at all (suite 2.40.3).
272
+ *
273
+ * The name filter above matches settings ABOUT secrets as well as secrets:
274
+ * openwop-app's `OPENWOP_WEBHOOK_SECRET_ROTATION_OVERLAP_S=60` (RFC 0201's
275
+ * rotation overlap) was swept in, and `scrubEvidence` then rewrote every "60"
276
+ * in the bundle — including the one inside `discovery.sha256`, which stopped
277
+ * matching `^[0-9a-f]{64}$` and failed the host's own deploy certify. A short
278
+ * value occurs by chance inside digests, counts and timestamps, so scrubbing it
279
+ * corrupts the evidence without protecting anything: no credential is a bare
280
+ * integer or under 8 characters (NIST SP 800-63B's floor for a memorized
281
+ * secret). The floor applies only to values GUESSED from variable names; a
282
+ * credential the emitter hands over explicitly (`extra`) is always scrubbed,
283
+ * whatever its shape.
284
+ */
285
+ export function credentialShaped(v) {
286
+ const t = v.trim();
287
+ return t.length >= 8 && !/^\d+$/.test(t);
288
+ }
270
289
  export function evidenceSecretsFromEnv(env, extra = [], except = []) {
271
290
  const keep = new Set(except.filter((s) => s !== undefined && s.trim() !== ''));
272
291
  const out = new Set();
@@ -277,8 +296,11 @@ export function evidenceSecretsFromEnv(env, extra = [], except = []) {
277
296
  continue;
278
297
  if (/_ID$/.test(k))
279
298
  continue; // a key's identifier is published, not secret
280
- if (v !== undefined && v.trim() !== '' && !keep.has(v))
281
- out.add(v);
299
+ if (v === undefined || v.trim() === '' || keep.has(v))
300
+ continue;
301
+ if (!credentialShaped(v))
302
+ continue; // a setting that names a secret, not a secret
303
+ out.add(v);
282
304
  }
283
305
  for (const s of extra)
284
306
  if (s !== undefined && s.trim() !== '' && !keep.has(s))
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.40.1",
4
- "stampSha256": "1fb5733bb565913ac21afc3e5179ea3cb4d1b98ab39f543fb3c70f9a936453bd"
3
+ "version": "2.40.3",
4
+ "stampSha256": "a3da70b5c189e692914fd3516f841bfaff477a365503009b761bccc1842f43e2"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.40.1",
3
+ "version": "2.40.3",
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.40.1"
59
+ "@openwop/spec-artifacts": "2.40.3"
60
60
  }
61
61
  }