@openwop/openwop-conformance 1.105.0 → 1.106.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "1.105.0",
3
+ "version": "1.106.1",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md \u00a7\"Resolving the contract\". Compare against the stamp in your installed @openwop/openwop-conformance to detect a stale hand-copied contract.",
3
- "suiteVersion": "1.105.0",
4
- "corpusCommit": "6eac39408ddd1007eab55e23a1cc09d2078d7ef5"
3
+ "suiteVersion": "1.106.1",
4
+ "corpusCommit": "eb487d75adb0178afdcb897e8602f6111a4099a9"
5
5
  }
@@ -14,10 +14,15 @@
14
14
  * 200 {"maxTokens":null}
15
15
  *
16
16
  * The registration path logged *"test seam ENABLED — NEVER enable in
17
- * production"* while doing exactly that, and the mock-AI staging endpoint
18
- * performed no tenant resolution at all — staging keyed on `nodeId`, which is
17
+ * production"* while doing exactly that. Staging is keyed on `nodeId`, which is
19
18
  * not a secret: node ids ship inside chain packs.
20
19
  *
20
+ * (An earlier account of this — including the first version of this docblock —
21
+ * said the staging route performed *no tenant resolution at all*. The reporter
22
+ * retracted that within the hour: auth **runs and succeeds**, minting an
23
+ * anonymous session. The corrected mechanism is what makes the weak prose
24
+ * clause interesting, so it is recorded rather than quietly swapped.)
25
+ *
21
26
  * **Why this matters beyond the seam.** A staged mock program makes a replay
22
27
  * diverge on purpose, so an unauthenticated caller could switch off the
23
28
  * byte-equivalence `replay.md` §C.2 requires — and any host advertising
@@ -30,6 +35,16 @@
30
35
  * enabled answers `404` and passes correctly, which is the honest outcome
31
36
  * rather than a skip.
32
37
  *
38
+ * **Why this leg asserts the observable property, not the mechanism.** The prose clause
39
+ * originally required an enabled seam to apply *"the same authentication and tenant
40
+ * resolution as the canonical surface"* — which RFC 0132 makes trivially satisfiable, since
41
+ * the canonical surface legitimately admits anonymous actors. The reporting host's auth
42
+ * **ran and succeeded**, minting `tenantId: "anon:<sid>"`, so the seam applied exactly the
43
+ * canonical treatment and the hole survived the rule. This leg reds it either way, because
44
+ * a `200` to a credential-less caller is the thing that matters. **The prose and this leg
45
+ * disagreed for twenty minutes and the leg was right** — the clause now requires a
46
+ * non-anonymous principal.
47
+ *
33
48
  * Requires a base URL; issues NO credentials by design.
34
49
  */
35
50
 
@@ -52,11 +67,41 @@ const SEAM_PROBES: readonly string[] = [
52
67
  /** 200 is the only unambiguous failure: the seam answered a stranger. */
53
68
  const ACCEPTABLE = new Set([401, 403, 404, 405, 501]);
54
69
 
70
+ /**
71
+ * A `200` can arrive from two very different places, and the difference decides
72
+ * what you should go fix.
73
+ *
74
+ * Every seam in `host-sample-test-seams.md` answers with a JSON body — the doc
75
+ * specifies each one's response shape as an object (`{ released, event }`,
76
+ * `{ ok: true, packId }`, `{ entries: … }`). **No seam answers with an HTML
77
+ * document.** So a `200 text/html` is not a seam that talked to a stranger; it
78
+ * is a static-hosting catch-all — an SPA rewrite in front of the API — matching
79
+ * a path the backend never saw.
80
+ *
81
+ * This was found by probing `app.openwop.dev` (Firebase Hosting, `**` rewrite
82
+ * to the SPA shell) instead of the backend's own origin. Every probe returned
83
+ * `200 text/html` with the app's `index.html`, which under the previous message
84
+ * read as *"an open control surface on a public origin"* — alarming, and wrong.
85
+ * The same paths on `…-backend-….run.app` return `404 application/json`.
86
+ *
87
+ * **Both cases still fail.** Nothing here relaxes the assertion, because the
88
+ * cheap way to silence a false alarm is to stop failing on `200`, and that would
89
+ * delete the check. What changes is the diagnosis: an HTML catch-all means the
90
+ * suite is pointed at the CDN rather than the API origin, which is its own
91
+ * defect — a run against the wrong origin cannot witness anything about the
92
+ * host, and every other leg in the suite is equally blind at that base URL.
93
+ */
94
+ function classify200(contentType: string | null): 'seam' | 'catchall' {
95
+ return (contentType ?? '').toLowerCase().includes('text/html') ? 'catchall' : 'seam';
96
+ }
97
+
55
98
  describe('test-seam-unauthenticated: an enabled seam still authenticates', () => {
56
99
  it('no /v1/host/sample/* seam answers an unauthenticated request with 200', async () => {
57
100
  const env = loadEnv();
58
101
  const answered: string[] = [];
59
102
  let probed = 0;
103
+ let seamAnswers = 0;
104
+ let catchAllAnswers = 0;
60
105
 
61
106
  for (const path of SEAM_PROBES) {
62
107
  // node:fetch directly with NO Authorization header — the driver's
@@ -68,8 +113,18 @@ describe('test-seam-unauthenticated: an enabled seam still authenticates', () =>
68
113
  continue; // connection-level refusal is a stronger answer than 404
69
114
  }
70
115
  probed += 1;
71
- if (res.status === 200) answered.push(`${path} -> 200`);
72
- else if (!ACCEPTABLE.has(res.status)) {
116
+ if (res.status === 200) {
117
+ const contentType = res.headers.get('content-type');
118
+ const kind = classify200(contentType);
119
+ if (kind === 'seam') seamAnswers += 1;
120
+ else catchAllAnswers += 1;
121
+ answered.push(
122
+ `${path} -> 200 (${contentType ?? 'no content-type'}) — ` +
123
+ (kind === 'seam'
124
+ ? 'SEAM ANSWERED A CREDENTIAL-LESS CALLER'
125
+ : 'HTML body: a static-hosting catch-all, not the API origin'),
126
+ );
127
+ } else if (!ACCEPTABLE.has(res.status)) {
73
128
  // Not a pass and not the known failure — record it rather than
74
129
  // silently tolerating a status nobody reasoned about.
75
130
  answered.push(`${path} -> ${res.status} (unexpected; expected one of ${[...ACCEPTABLE].join('/')})`);
@@ -85,16 +140,27 @@ describe('test-seam-unauthenticated: an enabled seam still authenticates', () =>
85
140
  'not looking, which is the failure mode it exists to prevent',
86
141
  ).toBeGreaterThan(0);
87
142
 
143
+ // The diagnosis is chosen from what the answers actually were. An HTML
144
+ // catch-all and an open seam are both failures and they are not the same
145
+ // bug, so the message must not name the wrong one — a check that reports a
146
+ // security finding for a misrouted base URL trains its reader to distrust it.
147
+ const diagnosis =
148
+ seamAnswers === 0 && catchAllAnswers > 0
149
+ ? 'WRONG ORIGIN, not an open seam. Every 200 above carried an HTML body, and no seam in ' +
150
+ 'host-sample-test-seams.md answers with HTML — so these are a static-hosting rewrite ' +
151
+ '(an SPA `**` catch-all) matching paths the backend never received. Point ' +
152
+ 'OPENWOP_BASE_URL at the API origin itself. This is still a failure: at this base URL ' +
153
+ 'no leg in the suite is witnessing the host, so a green run here would mean nothing.'
154
+ : 'An ENABLED seam MUST require an authenticated, NON-ANONYMOUS principal. A host that ' +
155
+ 'mints an anonymous identity for credential-less callers MUST NOT treat it as ' +
156
+ 'satisfying that. The env-gate governs whether a seam EXISTS; it does not govern who ' +
157
+ 'may call it. A seam answering a credential-less request with 200 is an open control ' +
158
+ 'surface on a public origin — and staging keys such as `nodeId` are not secrets, they ' +
159
+ 'ship inside chain packs.';
160
+
88
161
  expect(
89
162
  answered,
90
- driver.describe(
91
- 'host-sample-test-seams.md §"Production safety (normative)"',
92
- 'An ENABLED seam MUST apply the same authentication and tenant resolution as the canonical ' +
93
- 'surface. The env-gate governs whether a seam EXISTS; it does not govern who may call it. ' +
94
- 'A seam answering an unauthenticated request is an open control surface on a public ' +
95
- 'origin — and staging keys such as `nodeId` are not secrets, they ship inside chain ' +
96
- 'packs.\n ' + answered.join('\n '),
97
- ),
163
+ driver.describe('host-sample-test-seams.md §"Production safety (normative)"', `${diagnosis}\n ${answered.join('\n ')}`),
98
164
  ).toEqual([]);
99
165
  });
100
166
  });