@openwop/openwop-conformance 2.40.2 → 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,12 @@
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
+
3
10
  ## [2.40.2] — 2026-09-26 — production bundles stop carrying harness-issuer blocked rows, and a colocated companion is marked
4
11
 
5
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.
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.2 @openwop/spec-artifacts@2.40.2
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
  ```
@@ -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.2",
4
- "stampSha256": "5b784776f9088d58f1fd29e14efa113715876aac5940b728f775a779c59a962a"
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.2",
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.2"
59
+ "@openwop/spec-artifacts": "2.40.3"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -3,10 +3,10 @@
3
3
  "generatedFrom": "src/scenarios/*.test.ts",
4
4
  "counts": {
5
5
  "files": 636,
6
- "tests": 2437,
7
- "withStableId": 2437,
6
+ "tests": 2438,
7
+ "withStableId": 2438,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2331
9
+ "explicitIds": 2332
10
10
  },
11
11
  "records": [
12
12
  {
@@ -7170,9 +7170,38 @@
7170
7170
  ]
7171
7171
  },
7172
7172
  {
7173
- "id": "openwop.it.certification-bundle-redaction.evidencesecretsfromenv-never-scrubs-a-key-identifier-id-variables-are-excluded-a",
7173
+ "id": "openwop.it.certification-bundle-redaction.evidencesecretsfromenv-never-treats-a-setting-about-a-secret-as-one-a-bare-integ",
7174
7174
  "file": "certification-bundle-redaction.test.ts",
7175
7175
  "line": 157,
7176
+ "title": "evidenceSecretsFromEnv never treats a setting ABOUT a secret as one: a bare integer or a value under 8 characters is not swept from the environment",
7177
+ "explicitId": "openwop.it.certification-bundle-redaction.setting-about-a-secret-not-swept",
7178
+ "citations": [
7179
+ {
7180
+ "section": "RFC 0148 §C",
7181
+ "requirement": "a bare-integer setting whose NAME mentions a secret (a rotation overlap, a TTL) MUST NOT be swept as a credential"
7182
+ },
7183
+ {
7184
+ "section": "RFC 0148 §C",
7185
+ "requirement": "a value under 8 characters is not credential-shaped and is not swept from the environment"
7186
+ },
7187
+ {
7188
+ "section": "RFC 0148 §C",
7189
+ "requirement": "a credential-shaped value is still swept"
7190
+ },
7191
+ {
7192
+ "section": "RFC 0148 §C",
7193
+ "requirement": "a credential the emitter hands over explicitly is scrubbed whatever its shape"
7194
+ },
7195
+ {
7196
+ "section": "RFC 0148 §C",
7197
+ "requirement": "scrubbing MUST NOT corrupt a digest the bundle carries"
7198
+ }
7199
+ ]
7200
+ },
7201
+ {
7202
+ "id": "openwop.it.certification-bundle-redaction.evidencesecretsfromenv-never-scrubs-a-key-identifier-id-variables-are-excluded-a",
7203
+ "file": "certification-bundle-redaction.test.ts",
7204
+ "line": 180,
7176
7205
  "title": "evidenceSecretsFromEnv never scrubs a key IDENTIFIER: *_ID variables are excluded and `except` values stay visible whichever variable carried them",
7177
7206
  "explicitId": "openwop.it.certification-bundle-redaction.key-identifier-never-scrubbed",
7178
7207
  "citations": [
@@ -7189,7 +7218,7 @@
7189
7218
  {
7190
7219
  "id": "openwop.it.certification-bundle-redaction.verifybundlev2-rejects-a-bundle-carrying-the-conformance-canary-anywhere-and-acc",
7191
7220
  "file": "certification-bundle-redaction.test.ts",
7192
- "line": 177,
7221
+ "line": 200,
7193
7222
  "title": "verifyBundleV2 REJECTS a bundle carrying the conformance canary anywhere, and accepts the scrubbed twin",
7194
7223
  "explicitId": "openwop.it.certification-bundle-redaction.verifybundlev2-rejects-a-bundle-carrying-the-conformance-canary-anywhere-and-acc",
7195
7224
  "citations": [
@@ -7202,7 +7231,7 @@
7202
7231
  {
7203
7232
  "id": "openwop.it.certification-bundle-redaction.the-canary-in-a-discovery-document-key-is-still-a-rejection-keys-are-evidence",
7204
7233
  "file": "certification-bundle-redaction.test.ts",
7205
- "line": 192,
7234
+ "line": 215,
7206
7235
  "title": "the canary in a discovery-document KEY is still a rejection (keys are evidence)",
7207
7236
  "explicitId": "openwop.it.certification-bundle-redaction.the-canary-in-a-discovery-document-key-is-still-a-rejection-keys-are-evidence",
7208
7237
  "citations": [
@@ -7215,7 +7244,7 @@
7215
7244
  {
7216
7245
  "id": "openwop.it.certification-bundle-redaction.the-target-configuration-digest-is-a-digest-the-api-key-is-not-a-substring-of-an",
7217
7246
  "file": "certification-bundle-redaction.test.ts",
7218
- "line": 198,
7247
+ "line": 221,
7219
7248
  "title": "the target-configuration digest is a digest: the API key is not a substring of any string in the document",
7220
7249
  "explicitId": "openwop.it.certification-bundle-redaction.the-target-configuration-digest-is-a-digest-the-api-key-is-not-a-substring-of-an",
7221
7250
  "citations": [
@@ -32080,7 +32109,7 @@
32080
32109
  {
32081
32110
  "id": "openwop.it.v2-revocation-honored.a-revoked-next-request-credential-is-refused-on-the-next-request-with-credential",
32082
32111
  "file": "v2-revocation-honored.test.ts",
32083
- "line": 38,
32112
+ "line": 40,
32084
32113
  "title": "a revoked next-request credential is refused on the next request with credential_revoked",
32085
32114
  "explicitId": "openwop.requirement.0170.revocation-honored",
32086
32115
  "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.40.2",
5
- "corpusTag": "v2.40.2",
4
+ "version": "2.40.3",
5
+ "corpusTag": "v2.40.3",
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": "6366b5514be71098e9ea41fc81ff52bc39db1a547b353c0b344cc2a2bf5370aa",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "c92bde1fb2ac949edfb50519651ac3755cfc88b8a2a8785eab0e15bcb6a8dbb9",
13
- "api/v2/asyncapi.yaml": "87d5b9de8e3fa920a0ddecce7f51018f8f573b5ef14f954973a0776d3b9d9a0f",
14
- "api/v2/openapi.yaml": "532467b699c179c355349c8bb483a801156a68b003d02aa9d7cf576f867c3097",
12
+ "api/seams-v2.yaml": "96b5b49ba8aab1595b547d568ece7c4fb9fae9d7bdde29aff7c39f20ef31b120",
13
+ "api/v2/asyncapi.yaml": "cc4d94df8eb1e168f30f8c12279bc65cf34c948a5ed95cc062288728fc605618",
14
+ "api/v2/openapi.yaml": "5029e637cd61c900b169cb3f79e61e48b3ee89a6c3c11f3262158bf2fa910125",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -204,13 +204,13 @@
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": "a16e3a91b9121d7b6d58fd9f65a348f92a07636417406f56bc3173571e9d55ea",
207
+ "spec/v1/core-standard-manifest.json": "c8d01314ab747038fb9b3ef17552d336f61211591b4af07379e65f65a6e55454",
208
208
  "spec/v1/deprecations.json": "2d03f4729810280147ea08c630ea65434f2dc5370567d0aac41c3595f8337d40",
209
209
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
210
210
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
211
211
  "spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
212
212
  "spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
213
- "spec/v1/gaps.json": "3decfec271c30f796ba76586ca06ab31e6ea3b8bcae174c3af0b8bb1c19709b5",
213
+ "spec/v1/gaps.json": "41e5bbd50515eb08923a29a79dfeb41d045f2b28dec9b2af3473c18db148c7be",
214
214
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
215
215
  "spec/v1/migrations.json": "8839497d6a4830b53fa7867014a8336f0d725ef2346352e54fb580b7947897af",
216
216
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
@@ -240,7 +240,7 @@
240
240
  "spec/v2/core/security-defaults.md": "500471a8db7af9b776ac40b4a9a278c9f3d45e1c1dead880114e7fadc7bee37d",
241
241
  "spec/v2/core/tool-catalog.md": "35f1a3fd509db0fc0490ddf400686fe94dc4b20c98064d5ebe776da516153e60",
242
242
  "spec/v2/core/versioning.md": "0aded71d090c8358c3ce17763c120cfc1cc42531a5eaa1b8a2a7016ed9594069",
243
- "spec/v2/core/webhooks.md": "95880896e37e5b7096693a75203cf0ed70cefa09959230e17b45aa9193078eff",
243
+ "spec/v2/core/webhooks.md": "2a44b100c42dd841fe2b8f87179a981be6c0dd26608ce1b14e077318c4ac8201",
244
244
  "spec/v2/core/workflow-chain-packs.md": "ee45d3fede3bb6f0cc6abcf0d6c929c7cd213a0fbe862b738a998f929c4d1102",
245
245
  "spec/v2/corrections.json": "cc74ffde74384b0f261a4f625871d922e8661d654d647a8534e21036595cb280",
246
246
  "spec/v2/corrections.schema.json": "4ac595b9a6d7f66d53de03fe0edbe0dc58de46426932387bfdc0ceb3cf949a3e",
@@ -294,9 +294,9 @@
294
294
  "spec/v2/path-manifest.json": "ae9b56d03a701061fd48ad0b73cb0373493f6a4547da92763b422295b5e19535",
295
295
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
296
296
  "spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
297
- "spec/v2/release.json": "f04bc2406dfc3af40a495d8750349409f053bb56f009359ebc62a8877251c90f",
297
+ "spec/v2/release.json": "4c84997343779f46a54f9c4caf3d02714abc563f618b25729ca4b52468a23d39",
298
298
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
299
- "spec/v2/surface-baseline.json": "26f2a03c650413ac9c0d0b5facee47ee0eaebeb01d377c808fe6e737bb08687c"
299
+ "spec/v2/surface-baseline.json": "2f1da906609307ec885cb04c36d7e688cd3124933759c88dc9ae3fa0ee9ec88b"
300
300
  },
301
- "corpusCommit": "70c93c0fcd5c595893c1cac7cf1020d5c5835ec2"
301
+ "corpusCommit": "5d3d440cac757fb2b6c08452f64785eaeda086d3"
302
302
  }
@@ -354,6 +354,26 @@ export function scrubEvidence<T>(value: T, secrets: readonly string[]): ScrubRes
354
354
  * are excluded, and a caller may name values that MUST stay visible (`except`)
355
355
  * regardless of which variable carried them.
356
356
  */
357
+ /**
358
+ * Whether an environment VALUE can be a credential at all (suite 2.40.3).
359
+ *
360
+ * The name filter above matches settings ABOUT secrets as well as secrets:
361
+ * openwop-app's `OPENWOP_WEBHOOK_SECRET_ROTATION_OVERLAP_S=60` (RFC 0201's
362
+ * rotation overlap) was swept in, and `scrubEvidence` then rewrote every "60"
363
+ * in the bundle — including the one inside `discovery.sha256`, which stopped
364
+ * matching `^[0-9a-f]{64}$` and failed the host's own deploy certify. A short
365
+ * value occurs by chance inside digests, counts and timestamps, so scrubbing it
366
+ * corrupts the evidence without protecting anything: no credential is a bare
367
+ * integer or under 8 characters (NIST SP 800-63B's floor for a memorized
368
+ * secret). The floor applies only to values GUESSED from variable names; a
369
+ * credential the emitter hands over explicitly (`extra`) is always scrubbed,
370
+ * whatever its shape.
371
+ */
372
+ export function credentialShaped(v: string): boolean {
373
+ const t = v.trim();
374
+ return t.length >= 8 && !/^\d+$/.test(t);
375
+ }
376
+
357
377
  export function evidenceSecretsFromEnv(
358
378
  env: NodeJS.ProcessEnv,
359
379
  extra: readonly (string | undefined)[] = [],
@@ -365,7 +385,9 @@ export function evidenceSecretsFromEnv(
365
385
  if (!k.startsWith('OPENWOP_')) continue;
366
386
  if (!/(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/.test(k)) continue;
367
387
  if (/_ID$/.test(k)) continue; // a key's identifier is published, not secret
368
- if (v !== undefined && v.trim() !== '' && !keep.has(v)) out.add(v);
388
+ if (v === undefined || v.trim() === '' || keep.has(v)) continue;
389
+ if (!credentialShaped(v)) continue; // a setting that names a secret, not a secret
390
+ out.add(v);
369
391
  }
370
392
  for (const s of extra) if (s !== undefined && s.trim() !== '' && !keep.has(s)) out.add(s);
371
393
  out.add(CONFORMANCE_SECRET_CANARY);
@@ -141,17 +141,40 @@ describe('RFC 0148 §C — certification-bundle-redaction: secret canaries never
141
141
 
142
142
  it('evidenceSecretsFromEnv selects OPENWOP_* key/token/secret/password variables, the handed credential, and ALWAYS the canary', () => {
143
143
  const env = {
144
- OPENWOP_API_KEY: 'k1',
145
- OPENWOP_MESSAGING_BRIDGE_TOKEN: 't1',
146
- OPENWOP_DB_PASSWORD: 'p1',
147
- OPENWOP_CLIENT_SECRET: 's1',
144
+ OPENWOP_API_KEY: 'k1-api-key-value',
145
+ OPENWOP_MESSAGING_BRIDGE_TOKEN: 't1-bridge-token',
146
+ OPENWOP_DB_PASSWORD: 'p1-db-password',
147
+ OPENWOP_CLIENT_SECRET: 's1-client-secret',
148
148
  OPENWOP_BASE_URL: 'https://example.invalid', // not a secret
149
149
  OPENWOP_REQUIRE_BEHAVIOR: 'true',
150
150
  PATH: '/usr/bin',
151
151
  SOME_OTHER_TOKEN: 'not-ours',
152
152
  } as NodeJS.ProcessEnv;
153
153
  const secrets = evidenceSecretsFromEnv(env, [API_KEY, undefined, '']);
154
- expect(secrets.sort(), req('openwop.it.certification-bundle-redaction.evidencesecretsfromenv-selects-openwop-key-token-secret-password-variables-the-h', 'RFC 0148 §C', 'evidenceSecretsFromEnv selects OPENWOP_* key/token/secret/password variables, the handed credential, and ALWAYS the canary')).toEqual([API_KEY, 'k1', 'p1', 's1', 't1', CONFORMANCE_SECRET_CANARY].sort());
154
+ expect(secrets.sort(), req('openwop.it.certification-bundle-redaction.evidencesecretsfromenv-selects-openwop-key-token-secret-password-variables-the-h', 'RFC 0148 §C', 'evidenceSecretsFromEnv selects OPENWOP_* key/token/secret/password variables, the handed credential, and ALWAYS the canary')).toEqual([API_KEY, 'k1-api-key-value', 'p1-db-password', 's1-client-secret', 't1-bridge-token', CONFORMANCE_SECRET_CANARY].sort());
155
+ });
156
+
157
+ it('evidenceSecretsFromEnv never treats a setting ABOUT a secret as one: a bare integer or a value under 8 characters is not swept from the environment', () => {
158
+ // Suite 2.40.2: openwop-app set OPENWOP_WEBHOOK_SECRET_ROTATION_OVERLAP_S=60,
159
+ // the name matched SECRET, and scrubEvidence rewrote the "60" inside
160
+ // discovery.sha256 — the bundle failed ^[0-9a-f]{64}$ and main could not deploy.
161
+ const digest = '4b8d99c002a061a0255e8224ee683162007febadd251b2d171b47c0849695315';
162
+ const env = {
163
+ OPENWOP_WEBHOOK_SECRET_ROTATION_OVERLAP_S: '60',
164
+ OPENWOP_TOKEN_TTL_SECONDS_KEY: '86400000',
165
+ OPENWOP_SHORT_KEY: 'abc1234',
166
+ OPENWOP_REAL_SECRET: 'whsec_c2VjcmV0LXZhbHVl',
167
+ } as NodeJS.ProcessEnv;
168
+ const secrets = evidenceSecretsFromEnv(env, ['k9']);
169
+ const R = 'openwop.it.certification-bundle-redaction.setting-about-a-secret-not-swept';
170
+ expect(secrets, req(R, 'RFC 0148 §C', 'a bare-integer setting whose NAME mentions a secret (a rotation overlap, a TTL) MUST NOT be swept as a credential')).not.toContain('60');
171
+ expect(secrets).not.toContain('86400000');
172
+ expect(secrets, req(R, 'RFC 0148 §C', 'a value under 8 characters is not credential-shaped and is not swept from the environment')).not.toContain('abc1234');
173
+ expect(secrets, req(R, 'RFC 0148 §C', 'a credential-shaped value is still swept')).toContain('whsec_c2VjcmV0LXZhbHVl');
174
+ expect(secrets, req(R, 'RFC 0148 §C', 'a credential the emitter hands over explicitly is scrubbed whatever its shape')).toContain('k9');
175
+ const { value } = scrubEvidence({ discovery: { sha256: digest } }, secrets);
176
+ expect(value.discovery.sha256, req(R, 'RFC 0148 §C', 'scrubbing MUST NOT corrupt a digest the bundle carries')).toBe(digest);
177
+ expect(value.discovery.sha256).toMatch(/^[0-9a-f]{64}$/);
155
178
  });
156
179
 
157
180
  it('evidenceSecretsFromEnv never scrubs a key IDENTIFIER: *_ID variables are excluded and `except` values stay visible whichever variable carried them', () => {
@@ -162,14 +185,14 @@ describe('RFC 0148 §C — certification-bundle-redaction: secret canaries never
162
185
  OPENWOP_BUNDLE_SIGNING_KEY: '-----BEGIN PRIVATE KEY-----\nsecret\n-----END PRIVATE KEY-----',
163
186
  OPENWOP_BUNDLE_SIGNING_KEY_ID: 'myndhyve-bundle-2026-09',
164
187
  OPENWOP_BUNDLE_VERIFIER_KEY_ID: 'steward-verifier-1',
165
- OPENWOP_API_KEY: 'k1',
188
+ OPENWOP_API_KEY: 'k1-api-key-value',
166
189
  OPENWOP_ODDLY_NAMED_KEY: 'published-key-id-in-a-secret-shaped-variable',
167
190
  } as NodeJS.ProcessEnv;
168
191
  const secrets = evidenceSecretsFromEnv(env, [], ['published-key-id-in-a-secret-shaped-variable']);
169
192
  expect(secrets, req('openwop.it.certification-bundle-redaction.key-identifier-never-scrubbed', 'RFC 0168 §E.2', 'the keyId a host publishes in discovery.document.signingKeys[] MUST survive evidence scrubbing — a *_ID variable is an identifier, not a secret')).not.toContain('myndhyve-bundle-2026-09');
170
193
  expect(secrets).not.toContain('steward-verifier-1');
171
194
  expect(secrets, req('openwop.it.certification-bundle-redaction.key-identifier-never-scrubbed', 'RFC 0168 §E.2', 'a value the emitter names as `except` (the keyId it is about to publish) is never scrubbed, whichever variable carried it')).not.toContain('published-key-id-in-a-secret-shaped-variable');
172
- expect(secrets).toContain('k1');
195
+ expect(secrets).toContain('k1-api-key-value');
173
196
  expect(secrets.some((s) => s.includes('BEGIN PRIVATE KEY'))).toBe(true);
174
197
  expect(secrets).toContain(CONFORMANCE_SECRET_CANARY);
175
198
  });
@@ -11,7 +11,9 @@
11
11
  * drives `POST /conformance/seams/sample/auth/credential/{mint,revoke}` (the
12
12
  * v1-shaped address `/v1/host/sample/auth/credential/…` through `seamPath()`)
13
13
  * with `{ lane }` / `{ lane, credential }` and records `blocked` when the host
14
- * does not mount it. Windowed lanes (`exp-and-recheck`, `short-lived`, `rebind`)
14
+ * does not mount it. A mint answer carrying `presentation: { kind: "cookie", name }`
15
+ * (2.40.3) is presented as `Cookie: <name>=<credential>`, so a cookie-borne
16
+ * `session` lane is witnessed as its production credential travels; bearer otherwise. Windowed lanes (`exp-and-recheck`, `short-lived`, `rebind`)
15
17
  * have no wire-observable "next request" and record `inapplicable`.
16
18
  */
17
19
 
@@ -48,17 +50,22 @@ describe('v2 revocation-honored (RFC 0170 §B.3 — seam-gated)', () => {
48
50
  const minted = await http(() => driver.post(MINT, { lane }));
49
51
  if (minted === null) return softSkip('blocked', `${MINT} unreachable (fetch failed)`);
50
52
  if (minted.status === 404 || minted.status === 403) return seamAbsent(`${MINT} not mounted (${minted.status}) — the host advertises the seams profile but does not serve the RFC 0170 §B.3 credential mint/revoke seams (api/seams-v2.yaml mintLaneCredential / revokeLaneCredential; host-sample-test-seams.md)`);
51
- const credential = (minted.json as { credential?: unknown } | undefined)?.credential;
53
+ const body = minted.json as { credential?: unknown; presentation?: { kind?: unknown; name?: unknown } } | undefined;
54
+ const credential = body?.credential;
55
+ // 2.40.3 — a cookie-borne lane (a `session` lane) is presented the way its
56
+ // production credential is, `Cookie: <name>=<credential>`. Bearer otherwise.
57
+ const cookieName = body?.presentation?.kind === 'cookie' && typeof body.presentation.name === 'string' ? body.presentation.name : null;
58
+ const present = (c: unknown): Record<string, string> => (cookieName ? { Cookie: `${cookieName}=${String(c)}` } : { Authorization: `Bearer ${String(c)}` });
52
59
  expect(typeof credential, req('openwop.requirement.0170.revocation-honored', DOC, `the mint seam MUST answer { credential } for lane ${lane}`)).toBe('string');
53
60
 
54
61
  // Positive control — the minted credential authenticates before revocation.
55
- const before = await http(() => driver.get('/runs/openwop-conformance-tenant/revocationprobe0123456789', { authenticated: false, headers: { Authorization: `Bearer ${String(credential)}` } }));
62
+ const before = await http(() => driver.get('/runs/openwop-conformance-tenant/revocationprobe0123456789', { authenticated: false, headers: present(credential) }));
56
63
  expect(before !== null && before.status !== 401, req('openwop.requirement.0170.revocation-honored', DOC, `a freshly minted ${lane} credential MUST authenticate before revocation (got ${before?.status ?? 'no response'})`)).toBe(true);
57
64
 
58
65
  const revoked = await http(() => driver.post(REVOKE, { lane, credential }));
59
66
  expect(revoked !== null && revoked.status < 400, req('openwop.requirement.0170.revocation-honored', DOC, `the revoke seam MUST accept the credential it minted for lane ${lane} (got ${revoked?.status ?? 'no response'})`)).toBe(true);
60
67
 
61
- const after = await http(() => driver.get('/runs/openwop-conformance-tenant/revocationprobe0123456789', { authenticated: false, headers: { Authorization: `Bearer ${String(credential)}` } }));
68
+ const after = await http(() => driver.get('/runs/openwop-conformance-tenant/revocationprobe0123456789', { authenticated: false, headers: present(credential) }));
62
69
  expect(after?.status, req('openwop.requirement.0170.revocation-honored', DOC, `lane ${lane} MUST refuse a revoked credential on the next request with 401`)).toBe(401);
63
70
  expect(readErrorCode(after?.json), req('openwop.requirement.0170.revocation-honored', DOC, `the refusal code MUST be credential_revoked (lane ${lane})`)).toBe('credential_revoked');
64
71
  }