@clowder-ai/plugin-contract 0.1.0-beta.8 → 0.2.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.
Files changed (144) hide show
  1. package/dist/codegen/generate-contract.d.ts +7 -0
  2. package/dist/codegen/generate-contract.d.ts.map +1 -1
  3. package/dist/codegen/generate-contract.js +350 -19
  4. package/dist/codegen/generate-contract.js.map +1 -1
  5. package/dist/conformance/fixtures.d.ts +3 -1
  6. package/dist/conformance/fixtures.d.ts.map +1 -1
  7. package/dist/conformance/fixtures.js +2 -1
  8. package/dist/conformance/fixtures.js.map +1 -1
  9. package/dist/conformance/index.d.ts +2 -1
  10. package/dist/conformance/index.d.ts.map +1 -1
  11. package/dist/conformance/index.js +2 -1
  12. package/dist/conformance/index.js.map +1 -1
  13. package/dist/conformance/messaging-behavior-fixture.d.ts +12 -0
  14. package/dist/conformance/messaging-behavior-fixture.d.ts.map +1 -0
  15. package/dist/conformance/messaging-behavior-fixture.js +30 -0
  16. package/dist/conformance/messaging-behavior-fixture.js.map +1 -0
  17. package/dist/conformance/runner.d.ts.map +1 -1
  18. package/dist/conformance/runner.js +23 -3
  19. package/dist/conformance/runner.js.map +1 -1
  20. package/dist/conformance/stdio-harness/wire-shape.d.ts +1 -1
  21. package/dist/docx-materialization.d.ts +8 -0
  22. package/dist/docx-materialization.d.ts.map +1 -0
  23. package/dist/docx-materialization.js +19 -0
  24. package/dist/docx-materialization.js.map +1 -0
  25. package/dist/generated/contract.generated.d.ts +1665 -33
  26. package/dist/generated/contract.generated.d.ts.map +1 -1
  27. package/dist/generated/contract.generated.js +72 -3
  28. package/dist/generated/contract.generated.js.map +1 -1
  29. package/dist/index.d.ts +2 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +2 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/validation/catalog.d.ts +21 -0
  34. package/dist/validation/catalog.d.ts.map +1 -0
  35. package/dist/validation/catalog.js +140 -0
  36. package/dist/validation/catalog.js.map +1 -0
  37. package/dist/validation/cloud-conversation-host.d.ts +18 -0
  38. package/dist/validation/cloud-conversation-host.d.ts.map +1 -0
  39. package/dist/validation/cloud-conversation-host.js +83 -0
  40. package/dist/validation/cloud-conversation-host.js.map +1 -0
  41. package/dist/validation/companion-bridge.d.ts +6 -0
  42. package/dist/validation/companion-bridge.d.ts.map +1 -0
  43. package/dist/validation/companion-bridge.js +198 -0
  44. package/dist/validation/companion-bridge.js.map +1 -0
  45. package/dist/validation/docx-materialization.d.ts +6 -0
  46. package/dist/validation/docx-materialization.d.ts.map +1 -0
  47. package/dist/validation/docx-materialization.js +36 -0
  48. package/dist/validation/docx-materialization.js.map +1 -0
  49. package/dist/validation/index.d.ts +9 -0
  50. package/dist/validation/index.d.ts.map +1 -1
  51. package/dist/validation/index.js +9 -0
  52. package/dist/validation/index.js.map +1 -1
  53. package/dist/validation/manifest.d.ts.map +1 -1
  54. package/dist/validation/manifest.js +324 -1
  55. package/dist/validation/manifest.js.map +1 -1
  56. package/dist/validation/members.d.ts +4 -0
  57. package/dist/validation/members.d.ts.map +1 -0
  58. package/dist/validation/members.js +11 -0
  59. package/dist/validation/members.js.map +1 -0
  60. package/dist/validation/messaging-semantic.d.ts.map +1 -1
  61. package/dist/validation/messaging-semantic.js +98 -0
  62. package/dist/validation/messaging-semantic.js.map +1 -1
  63. package/dist/validation/messaging-wire.d.ts +42 -0
  64. package/dist/validation/messaging-wire.d.ts.map +1 -0
  65. package/dist/validation/messaging-wire.js +184 -0
  66. package/dist/validation/messaging-wire.js.map +1 -0
  67. package/dist/validation/operation-rows.d.ts +25 -0
  68. package/dist/validation/operation-rows.d.ts.map +1 -0
  69. package/dist/validation/operation-rows.js +135 -0
  70. package/dist/validation/operation-rows.js.map +1 -0
  71. package/dist/validation/signals.d.ts +17 -0
  72. package/dist/validation/signals.d.ts.map +1 -0
  73. package/dist/validation/signals.js +32 -0
  74. package/dist/validation/signals.js.map +1 -0
  75. package/dist/variant.d.ts +114 -0
  76. package/dist/variant.d.ts.map +1 -0
  77. package/dist/variant.js +305 -0
  78. package/dist/variant.js.map +1 -0
  79. package/dist/wire/constants.d.ts +2 -1
  80. package/dist/wire/constants.d.ts.map +1 -1
  81. package/dist/wire/constants.js +2 -1
  82. package/dist/wire/constants.js.map +1 -1
  83. package/dist/wire/dispatch.d.ts +49 -0
  84. package/dist/wire/dispatch.d.ts.map +1 -0
  85. package/dist/wire/dispatch.js +880 -0
  86. package/dist/wire/dispatch.js.map +1 -0
  87. package/dist/wire/disposition-fixtures.d.ts +10 -0
  88. package/dist/wire/disposition-fixtures.d.ts.map +1 -1
  89. package/dist/wire/disposition-fixtures.js +661 -75
  90. package/dist/wire/disposition-fixtures.js.map +1 -1
  91. package/dist/wire/envelope.d.ts +31 -0
  92. package/dist/wire/envelope.d.ts.map +1 -1
  93. package/dist/wire/envelope.js +48 -1
  94. package/dist/wire/envelope.js.map +1 -1
  95. package/dist/wire/errors.d.ts +31 -1
  96. package/dist/wire/errors.d.ts.map +1 -1
  97. package/dist/wire/errors.js +36 -0
  98. package/dist/wire/errors.js.map +1 -1
  99. package/dist/wire/grants.d.ts +4 -4
  100. package/dist/wire/grants.js +3 -3
  101. package/dist/wire/handshake-byte-bounds.d.ts.map +1 -1
  102. package/dist/wire/handshake-byte-bounds.js +5 -2
  103. package/dist/wire/handshake-byte-bounds.js.map +1 -1
  104. package/dist/wire/handshake.d.ts +6 -0
  105. package/dist/wire/handshake.d.ts.map +1 -1
  106. package/dist/wire/handshake.js +6 -3
  107. package/dist/wire/handshake.js.map +1 -1
  108. package/dist/wire/index.d.ts +13 -6
  109. package/dist/wire/index.d.ts.map +1 -1
  110. package/dist/wire/index.js +31 -6
  111. package/dist/wire/index.js.map +1 -1
  112. package/dist/wire/lifecycle-byte-bounds.d.ts +40 -0
  113. package/dist/wire/lifecycle-byte-bounds.d.ts.map +1 -0
  114. package/dist/wire/lifecycle-byte-bounds.js +149 -0
  115. package/dist/wire/lifecycle-byte-bounds.js.map +1 -0
  116. package/dist/wire/messaging-byte-bounds.d.ts +62 -0
  117. package/dist/wire/messaging-byte-bounds.d.ts.map +1 -0
  118. package/dist/wire/messaging-byte-bounds.js +581 -0
  119. package/dist/wire/messaging-byte-bounds.js.map +1 -0
  120. package/dist/wire/registry.d.ts +63 -77
  121. package/dist/wire/registry.d.ts.map +1 -1
  122. package/dist/wire/registry.js +47 -76
  123. package/dist/wire/registry.js.map +1 -1
  124. package/dist/wire/row-shapes.d.ts +87 -70
  125. package/dist/wire/row-shapes.d.ts.map +1 -1
  126. package/dist/wire/row-shapes.js +31 -4
  127. package/dist/wire/row-shapes.js.map +1 -1
  128. package/fixtures/behavior/messaging/adversarial-invariants.json +92 -2
  129. package/fixtures/manifest/invalid/duplicate-signal-type.json +35 -0
  130. package/fixtures/manifest/invalid/icon-package-escape.json +22 -0
  131. package/fixtures/manifest/valid/localized-product-metadata.json +27 -0
  132. package/fixtures/messaging/valid/subscription-delivery.json +1 -1
  133. package/fixtures/signals/invalid/dynamic-schema-url.json +10 -0
  134. package/fixtures/signals/invalid/forbidden-raw-privacy.json +10 -0
  135. package/fixtures/signals/valid/remote-service-declaration.json +10 -0
  136. package/package.json +17 -3
  137. package/src/schemas/behavior-fixture.schema.json +903 -116
  138. package/src/schemas/catalog.schema.json +91 -0
  139. package/src/schemas/companion-bridge.schema.json +2256 -0
  140. package/src/schemas/docx-materialization.schema.json +302 -0
  141. package/src/schemas/manifest.schema.json +2276 -88
  142. package/src/schemas/messaging.schema.json +692 -41
  143. package/src/schemas/plugin-metadata.schema.json +62 -0
  144. package/src/schemas/signal.schema.json +119 -0
@@ -0,0 +1,880 @@
1
+ /**
2
+ * Wire dispatch classifier — pre-dispatch frame classification per the
3
+ * disposition table (T-A through T-M, §3.8-1 plus beta.8 closure).
4
+ *
5
+ * This module classifies decoded NDJSON frames into one of the 13
6
+ * disposition classes. T-A (transport failure) and T-B (JSON parse error)
7
+ * are handled by the NDJSON frame decoder layer — this classifier covers
8
+ * T-C through T-M.
9
+ *
10
+ * A frame that passes all rejection checks returns disposition=null with
11
+ * outcome='accept', indicating a valid request that should be dispatched
12
+ * to a method handler.
13
+ *
14
+ * This module lives in @clowder-ai/plugin-contract (F202 C1: the wire
15
+ * frame classifier was moved here from @clowder-ai/plugin-sdk so the
16
+ * dependency arrow is 插件 → SDK → contract ← Host). It defines nothing
17
+ * new — every classification rule traces to the frozen disposition table
18
+ * in this package, and every key set it uses is co-located with or
19
+ * generated from the contract types it mirrors.
20
+ */
21
+ import { PING_NONCE_MIN_LENGTH, PING_NONCE_MAX_LENGTH, PING_INPUT_KEYS, PING_RESULT_KEYS, } from './row-shapes.js';
22
+ import { ERROR_BODY_APPLICATION_KEYS, ERROR_BODY_STANDARD_KEYS, REASON_DATA_KEYS, CODE_DATA_KEYS, HANDSHAKE_REJECTED_CODE, HANDSHAKE_REJECTED_MESSAGE, DELIVERY_REJECTED_CODE, DOMAIN_ERROR_CODE, DEADLINE_EXPIRED_CODE, SNAPSHOT_UNAVAILABLE_CODE, PARSE_ERROR_CODE, INVALID_REQUEST_CODE, INVALID_REQUEST_MESSAGE, METHOD_NOT_FOUND_CODE, METHOD_NOT_FOUND_MESSAGE, INVALID_PARAMS_CODE, INVALID_PARAMS_MESSAGE, ALL_ERROR_CODES, APPLICATION_ERROR_CODES, ERROR_CODE_TO_MESSAGE, HANDSHAKE_REJECT_REASONS, DELIVERY_REJECT_REASONS, LIFECYCLE_REJECT_REASONS, SNAPSHOT_UNAVAILABLE_REASONS, } from './errors.js';
23
+ import { REQUEST_ALLOWED_KEYS, NOTIFICATION_ALLOWED_KEYS, RESPONSE_SUCCESS_KEYS, RESPONSE_ERROR_KEYS, PARAMS_ALLOWED_KEYS, META_ALLOWED_KEYS, } from './envelope.js';
24
+ import { hasHandshakeAuthorityInjection, validateCandidateHello, validateBrokerReadyParams, validateSessionBinding, } from './handshake.js';
25
+ import { validateEffectiveGrants } from './grants.js';
26
+ import { validateRequestId } from './request-id.js';
27
+ import { isWireUInt53, isCanonicalUInt53Token } from './wire-uint53.js';
28
+ import { WIRE_METHOD_REGISTRY, NOTIFICATION_METHODS, isWireMethod, } from './registry.js';
29
+ import { MESSAGING_ROW_METHODS, validateMessagingRowInput, validateMessagingRowResult, } from '../validation/messaging-wire.js';
30
+ import {} from '../validation/signals.js';
31
+ import { MESSAGING_ERROR_CODE_VALUES } from '../generated/contract.generated.js';
32
+ // ---------------------------------------------------------------------------
33
+ // Error response builders
34
+ // ---------------------------------------------------------------------------
35
+ function respondInvalidRequestNull() {
36
+ return {
37
+ disposition: 'T-D',
38
+ outcome: 'respond',
39
+ response: {
40
+ jsonrpc: '2.0',
41
+ id: null,
42
+ error: { code: INVALID_REQUEST_CODE, message: INVALID_REQUEST_MESSAGE },
43
+ },
44
+ };
45
+ }
46
+ function respondInvalidRequestId(id) {
47
+ return {
48
+ disposition: 'T-F',
49
+ outcome: 'respond',
50
+ response: {
51
+ jsonrpc: '2.0',
52
+ id,
53
+ error: { code: INVALID_REQUEST_CODE, message: INVALID_REQUEST_MESSAGE },
54
+ },
55
+ };
56
+ }
57
+ function respondMethodNotFound(id) {
58
+ return {
59
+ disposition: 'T-F',
60
+ outcome: 'respond',
61
+ response: {
62
+ jsonrpc: '2.0',
63
+ id,
64
+ error: { code: METHOD_NOT_FOUND_CODE, message: METHOD_NOT_FOUND_MESSAGE },
65
+ },
66
+ };
67
+ }
68
+ function respondInvalidParams(id) {
69
+ return {
70
+ disposition: 'T-F',
71
+ outcome: 'respond',
72
+ response: {
73
+ jsonrpc: '2.0',
74
+ id,
75
+ error: { code: INVALID_PARAMS_CODE, message: INVALID_PARAMS_MESSAGE },
76
+ },
77
+ };
78
+ }
79
+ function respondInvalidParamsValue(id) {
80
+ return {
81
+ disposition: 'T-G',
82
+ outcome: 'respond',
83
+ response: {
84
+ jsonrpc: '2.0',
85
+ id,
86
+ error: { code: INVALID_PARAMS_CODE, message: INVALID_PARAMS_MESSAGE },
87
+ },
88
+ };
89
+ }
90
+ function respondHandshakeAuthorityViolation(id) {
91
+ return {
92
+ disposition: 'T-G',
93
+ outcome: 'respond',
94
+ response: {
95
+ jsonrpc: '2.0',
96
+ id,
97
+ error: {
98
+ code: HANDSHAKE_REJECTED_CODE,
99
+ message: HANDSHAKE_REJECTED_MESSAGE,
100
+ data: { reason: 'AUTHORITY_VIOLATION' },
101
+ },
102
+ },
103
+ };
104
+ }
105
+ function close(disposition) {
106
+ return { disposition, outcome: 'close' };
107
+ }
108
+ function accept(disposition) {
109
+ return { disposition, outcome: 'accept' };
110
+ }
111
+ // ---------------------------------------------------------------------------
112
+ // Non-scalar string detection (T-C canonicality, lone surrogate check)
113
+ // ---------------------------------------------------------------------------
114
+ /**
115
+ * Returns true if any string value or object key in the parsed JSON tree
116
+ * contains a lone surrogate (U+D800–U+DFFF). These are non-scalar strings
117
+ * per the Unicode specification and fail the T-C canonicality predicate.
118
+ *
119
+ * Lone surrogates roundtrip through JSON.stringify (ES2019+ escapes them
120
+ * as \uXXXX), so byte-equality alone cannot catch them.
121
+ */
122
+ function containsNonScalarString(value) {
123
+ if (typeof value === 'string') {
124
+ for (let i = 0; i < value.length; i++) {
125
+ const c = value.charCodeAt(i);
126
+ if (c >= 0xD800 && c <= 0xDBFF) {
127
+ // High surrogate — must be followed by a low surrogate (U+DC00–U+DFFF)
128
+ const next = i + 1 < value.length ? value.charCodeAt(i + 1) : 0;
129
+ if (next < 0xDC00 || next > 0xDFFF)
130
+ return true;
131
+ i++; // Skip the valid low surrogate pair partner
132
+ }
133
+ else if (c >= 0xDC00 && c <= 0xDFFF) {
134
+ // Lone low surrogate (not preceded by a high surrogate)
135
+ return true;
136
+ }
137
+ }
138
+ return false;
139
+ }
140
+ if (value === null || typeof value !== 'object')
141
+ return false;
142
+ if (Array.isArray(value)) {
143
+ for (const item of value) {
144
+ if (containsNonScalarString(item))
145
+ return true;
146
+ }
147
+ return false;
148
+ }
149
+ for (const key of Object.keys(value)) {
150
+ if (containsNonScalarString(key))
151
+ return true;
152
+ if (containsNonScalarString(value[key]))
153
+ return true;
154
+ }
155
+ return false;
156
+ }
157
+ // ---------------------------------------------------------------------------
158
+ // WireUInt53 raw-token validation (P1-1 maintainer requirement)
159
+ // ---------------------------------------------------------------------------
160
+ /**
161
+ * Check whether any WireUInt53 position in the parsed frame value has a
162
+ * numeric token whose V8-canonical form violates the WireUInt53 raw grammar
163
+ * (0|[1-9][0-9]{0,15}, no sign, no decimal, no exponent).
164
+ *
165
+ * After byte-equality passes (rawStr === JSON.stringify(value)), the raw
166
+ * token at any numeric position IS String(parsedValue). So we walk the
167
+ * parsed structure to WireUInt53 positions and validate String(n) against
168
+ * isCanonicalUInt53Token. This catches negative integers (-1), fractions
169
+ * (1.5), and oversized values (>2^53-1) at the T-C layer, before any
170
+ * method-specific validation runs.
171
+ *
172
+ * WireUInt53 positions in the frozen schema:
173
+ * - params.meta.deadlineUnixMs (every request/notification)
174
+ * - params.input.baseRevision (messaging.appendElements input)
175
+ * - params.input.limit (messaging.read input)
176
+ * - params.input.maxItems (messaging.snapshot input)
177
+ * - params.input.envelope.revision (host.messaging.deliver input)
178
+ * - result.grantRevision (broker.hello SessionBinding)
179
+ * - result.revision (messaging.send, messaging.appendElements)
180
+ * - result.publishSequence (messaging.send)
181
+ * - result.appendSequence (messaging.appendElements)
182
+ * - result.events[].sequence (messaging.read)
183
+ * - result.items[].revision (messaging.snapshot)
184
+ */
185
+ function hasNonCanonicalUInt53Token(value, inFlight) {
186
+ const requestMethod = typeof value.method === 'string'
187
+ ? value.method
188
+ : undefined;
189
+ const params = value.params;
190
+ if (requestMethod !== undefined && params !== null && typeof params === 'object' && !Array.isArray(params)) {
191
+ const paramsObj = params;
192
+ // ── params.meta.deadlineUnixMs ──
193
+ const meta = paramsObj.meta;
194
+ if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
195
+ const metaObj = meta;
196
+ if (typeof metaObj.deadlineUnixMs === 'number') {
197
+ if (!isCanonicalUInt53Token(String(metaObj.deadlineUnixMs)))
198
+ return true;
199
+ }
200
+ }
201
+ // ── method-owned input WireUInt53 leaves ──
202
+ const input = paramsObj.input;
203
+ if (input !== null && typeof input === 'object' && !Array.isArray(input)) {
204
+ const inputObj = input;
205
+ // ── M0-C messaging request WireUInt53 leaves ──
206
+ if (requestMethod === 'messaging.appendElements' && typeof inputObj.baseRevision === 'number') {
207
+ if (!isCanonicalUInt53Token(String(inputObj.baseRevision)))
208
+ return true;
209
+ }
210
+ if (requestMethod === 'messaging.read' && typeof inputObj.limit === 'number') {
211
+ if (!isCanonicalUInt53Token(String(inputObj.limit)))
212
+ return true;
213
+ }
214
+ if (requestMethod === 'messaging.snapshot' && typeof inputObj.maxItems === 'number') {
215
+ if (!isCanonicalUInt53Token(String(inputObj.maxItems)))
216
+ return true;
217
+ }
218
+ if (requestMethod === 'host.messaging.deliver') {
219
+ const envelope = inputObj.envelope;
220
+ if (envelope !== null && typeof envelope === 'object' && !Array.isArray(envelope)) {
221
+ const envelopeObj = envelope;
222
+ if (typeof envelopeObj.revision === 'number') {
223
+ if (!isCanonicalUInt53Token(String(envelopeObj.revision)))
224
+ return true;
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ // Response-side WireUInt53 positions: consult the correlated in-flight
231
+ // row before applying raw-token gates. Only protocol-defined numeric
232
+ // leaves at known positions are checked — open payloads are not covered.
233
+ const result = value.result;
234
+ const responseMethod = typeof value.id === 'string'
235
+ ? inFlight.get(value.id)?.method
236
+ : undefined;
237
+ if (!('method' in value) && result !== null && typeof result === 'object' && !Array.isArray(result)) {
238
+ const resultObj = result;
239
+ // H7: broker.hello SessionBinding.grantRevision
240
+ if (responseMethod === 'broker.hello') {
241
+ if (typeof resultObj.grantRevision === 'number') {
242
+ if (!isCanonicalUInt53Token(String(resultObj.grantRevision)))
243
+ return true;
244
+ }
245
+ }
246
+ // messaging.send: result.revision, result.publishSequence
247
+ if (responseMethod === 'messaging.send') {
248
+ if (typeof resultObj.revision === 'number' && !isCanonicalUInt53Token(String(resultObj.revision)))
249
+ return true;
250
+ if (typeof resultObj.publishSequence === 'number' && !isCanonicalUInt53Token(String(resultObj.publishSequence)))
251
+ return true;
252
+ }
253
+ // messaging.appendElements: result.revision, result.appendSequence
254
+ if (responseMethod === 'messaging.appendElements') {
255
+ if (typeof resultObj.revision === 'number' && !isCanonicalUInt53Token(String(resultObj.revision)))
256
+ return true;
257
+ if (typeof resultObj.appendSequence === 'number' && !isCanonicalUInt53Token(String(resultObj.appendSequence)))
258
+ return true;
259
+ }
260
+ // messaging.read: result.events[] WireUInt53 leaves
261
+ // Common: events[].sequence
262
+ // Publish arm: events[].envelope.revision (not traversing open payload)
263
+ // Elements-append arm: events[].revision, events[].baseRevision
264
+ if (responseMethod === 'messaging.read' && Array.isArray(resultObj.events)) {
265
+ for (const event of resultObj.events) {
266
+ if (event !== null && typeof event === 'object' && !Array.isArray(event)) {
267
+ const eventObj = event;
268
+ if (typeof eventObj.sequence === 'number' && !isCanonicalUInt53Token(String(eventObj.sequence)))
269
+ return true;
270
+ // publish arm: envelope.revision
271
+ const eventEnvelope = eventObj.envelope;
272
+ if (eventEnvelope !== null && typeof eventEnvelope === 'object' && !Array.isArray(eventEnvelope)) {
273
+ const envObj = eventEnvelope;
274
+ if (typeof envObj.revision === 'number' && !isCanonicalUInt53Token(String(envObj.revision)))
275
+ return true;
276
+ }
277
+ // elements-append arm: revision, baseRevision
278
+ if (typeof eventObj.revision === 'number' && !isCanonicalUInt53Token(String(eventObj.revision)))
279
+ return true;
280
+ if (typeof eventObj.baseRevision === 'number' && !isCanonicalUInt53Token(String(eventObj.baseRevision)))
281
+ return true;
282
+ }
283
+ }
284
+ }
285
+ // messaging.snapshot: result.items[].revision
286
+ if (responseMethod === 'messaging.snapshot' && Array.isArray(resultObj.items)) {
287
+ for (const item of resultObj.items) {
288
+ if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
289
+ const itemObj = item;
290
+ if (typeof itemObj.revision === 'number' && !isCanonicalUInt53Token(String(itemObj.revision)))
291
+ return true;
292
+ }
293
+ }
294
+ }
295
+ }
296
+ return false;
297
+ }
298
+ // ---------------------------------------------------------------------------
299
+ // Closed-set constants derived from contract runtime exports
300
+ //
301
+ // Every key set below is co-located with or generated from the contract
302
+ // type it mirrors (F202 C1: former contract-mirror.ts moved into the
303
+ // contract). MESSAGING_ERROR_CODE_SET is a performance wrapper over the
304
+ // generated MESSAGING_ERROR_CODE_VALUES array.
305
+ // ---------------------------------------------------------------------------
306
+ const MESSAGING_ERROR_CODE_SET = new Set(MESSAGING_ERROR_CODE_VALUES);
307
+ // ---------------------------------------------------------------------------
308
+ // Derived constants (built from contract runtime imports, NOT mirrors)
309
+ // ---------------------------------------------------------------------------
310
+ // Error code validation sets (built from contract arrays).
311
+ const KNOWN_ERROR_CODES = new Set(ALL_ERROR_CODES);
312
+ const APPLICATION_CODES = new Set(APPLICATION_ERROR_CODES);
313
+ // Reason enum sets (built from contract arrays).
314
+ const HANDSHAKE_REASONS = new Set(HANDSHAKE_REJECT_REASONS);
315
+ const DELIVERY_REASONS = new Set(DELIVERY_REJECT_REASONS);
316
+ const LIFECYCLE_REASONS = new Set(LIFECYCLE_REJECT_REASONS);
317
+ const SNAPSHOT_REASONS = new Set(SNAPSHOT_UNAVAILABLE_REASONS);
318
+ const MESSAGING_METHODS = new Set(MESSAGING_ROW_METHODS);
319
+ function isMessagingRowMethod(method) {
320
+ return MESSAGING_METHODS.has(method);
321
+ }
322
+ // Standard error codes that mandate null id (not string RequestId).
323
+ // ParseError (-32700) ALWAYS has id: null per the contract envelope.
324
+ // If we reach the error validation path, id is already a valid string
325
+ // (verified upstream), so ParseError with string id is invalid.
326
+ const NULL_ID_ERROR_CODES = new Set([PARSE_ERROR_CODE]);
327
+ // ---------------------------------------------------------------------------
328
+ // Per-method application error allowlists (frozen per-row registry)
329
+ // ---------------------------------------------------------------------------
330
+ //
331
+ // Every registry row's application-error set resolves through the closed
332
+ // application table in #1165. Eligibility is keyed off the row, NOT
333
+ // leafClosure — eligibility is a per-row contract property.
334
+ //
335
+ // Rows 1-2 (broker.hello/ready): HANDSHAKE_REJECTED
336
+ // Rows 3-7 (messaging send/append/sub/read/ack): DOMAIN_ERROR, DEADLINE_EXPIRED
337
+ // Row 8 (messaging.snapshot): DOMAIN_ERROR, DEADLINE_EXPIRED, SNAPSHOT_UNAVAILABLE
338
+ // Row 9 (host.messaging.deliver): DELIVERY_REJECTED
339
+ // Row 11 (host.lifecycle.ping): standard only (no application errors)
340
+ //
341
+ // Standard errors are always allowed on every row. Application error
342
+ // codes NOT in the per-row allowlist → T-H.
343
+ const EMPTY_ERROR_SET = new Set();
344
+ const HANDSHAKE_ERROR_SET = new Set([HANDSHAKE_REJECTED_CODE]);
345
+ const MESSAGING_ERROR_SET = new Set([DOMAIN_ERROR_CODE, DEADLINE_EXPIRED_CODE]);
346
+ const SNAPSHOT_ERROR_SET = new Set([DOMAIN_ERROR_CODE, DEADLINE_EXPIRED_CODE, SNAPSHOT_UNAVAILABLE_CODE]);
347
+ const DELIVERY_ERROR_SET = new Set([DELIVERY_REJECTED_CODE]);
348
+ const DEADLINE_ONLY_SET = new Set([DEADLINE_EXPIRED_CODE]);
349
+ const METHOD_APPLICATION_ERROR_ALLOW = {
350
+ 'broker.hello': HANDSHAKE_ERROR_SET,
351
+ 'broker.ready': HANDSHAKE_ERROR_SET,
352
+ 'messaging.send': MESSAGING_ERROR_SET,
353
+ 'messaging.appendElements': MESSAGING_ERROR_SET,
354
+ 'messaging.subscribe': MESSAGING_ERROR_SET,
355
+ 'messaging.read': MESSAGING_ERROR_SET,
356
+ 'messaging.ack': MESSAGING_ERROR_SET,
357
+ 'messaging.snapshot': SNAPSHOT_ERROR_SET,
358
+ 'host.messaging.deliver': DELIVERY_ERROR_SET,
359
+ 'host.lifecycle.ping': EMPTY_ERROR_SET, // standard only
360
+ 'streams.read': MESSAGING_ERROR_SET,
361
+ };
362
+ // ---------------------------------------------------------------------------
363
+ // Response candidate sub-classifier (T-H / T-L)
364
+ // ---------------------------------------------------------------------------
365
+ function classifyResponseCandidate(value, inFlight) {
366
+ const hasResult = 'result' in value;
367
+ const hasError = 'error' in value;
368
+ // ── Closed envelope structure ──────────────────────────────────────
369
+ if (value.jsonrpc !== '2.0')
370
+ return close('T-H');
371
+ // Mutual exclusivity: exactly one of result/error
372
+ if (hasResult && hasError)
373
+ return close('T-H');
374
+ // Closed outer keys: no additional members
375
+ const allowedKeys = hasResult ? RESPONSE_SUCCESS_KEYS : RESPONSE_ERROR_KEYS;
376
+ for (const key of Object.keys(value)) {
377
+ if (!allowedKeys.has(key))
378
+ return close('T-H');
379
+ }
380
+ // ── id validation ──────────────────────────────────────────────────
381
+ if (!('id' in value))
382
+ return close('T-H');
383
+ const id = value.id;
384
+ if (typeof id !== 'string')
385
+ return close('T-H');
386
+ if (validateRequestId(id) === null)
387
+ return close('T-H');
388
+ // ── In-flight correlation ──────────────────────────────────────────
389
+ const inFlightEntry = inFlight.get(id);
390
+ if (inFlightEntry === undefined)
391
+ return close('T-H');
392
+ // ── Error body: closed union validation ────────────────────────────
393
+ if (hasError) {
394
+ const error = value.error;
395
+ if (error === null || typeof error !== 'object' || Array.isArray(error)) {
396
+ return close('T-H');
397
+ }
398
+ const errObj = error;
399
+ // code: number, message: string (structural)
400
+ if (typeof errObj.code !== 'number')
401
+ return close('T-H');
402
+ if (typeof errObj.message !== 'string')
403
+ return close('T-H');
404
+ // Error code must be from the closed set
405
+ if (!KNOWN_ERROR_CODES.has(errObj.code))
406
+ return close('T-H');
407
+ // Code → message canonical mapping
408
+ const expectedMessage = ERROR_CODE_TO_MESSAGE[errObj.code];
409
+ if (errObj.message !== expectedMessage)
410
+ return close('T-H');
411
+ // Standard vs application error body structure
412
+ if (APPLICATION_CODES.has(errObj.code)) {
413
+ // Application errors (-32090..-32094) MUST have `data` (object)
414
+ if (!('data' in errObj))
415
+ return close('T-H');
416
+ if (errObj.data === null || typeof errObj.data !== 'object' || Array.isArray(errObj.data)) {
417
+ return close('T-H');
418
+ }
419
+ // Closed keys: {code, message, data} only
420
+ for (const key of Object.keys(errObj)) {
421
+ if (!ERROR_BODY_APPLICATION_KEYS.has(key))
422
+ return close('T-H');
423
+ }
424
+ // Per-arm data schema validation
425
+ const dataCheck = validateApplicationErrorData(errObj.code, errObj.data, inFlightEntry.method);
426
+ if (dataCheck !== null)
427
+ return dataCheck;
428
+ }
429
+ else {
430
+ // Standard errors: ParseError (-32700) mandates id: null.
431
+ // We already validated id is a string (not null) upstream.
432
+ // Therefore ParseError with string id is a protocol violation.
433
+ if (NULL_ID_ERROR_CODES.has(errObj.code))
434
+ return close('T-H');
435
+ // Standard errors (-32700..-32603) MUST NOT have `data`
436
+ if ('data' in errObj)
437
+ return close('T-H');
438
+ // Closed keys: {code, message} only
439
+ for (const key of Object.keys(errObj)) {
440
+ if (!ERROR_BODY_STANDARD_KEYS.has(key))
441
+ return close('T-H');
442
+ }
443
+ }
444
+ // ── Per-method error code restriction ────────────────────────────
445
+ // Application errors are only valid on methods whose frozen per-row
446
+ // error set includes them. Standard errors are always allowed.
447
+ // The complete map covers all 13 rows — no fallback needed.
448
+ if (APPLICATION_CODES.has(errObj.code)) {
449
+ if (!METHOD_APPLICATION_ERROR_ALLOW[inFlightEntry.method].has(errObj.code)) {
450
+ return close('T-H');
451
+ }
452
+ }
453
+ return accept('T-L');
454
+ }
455
+ // ── Success result: method-specific shape validation ───────────────
456
+ const resultCheck = validateResponseResult(value.result, inFlightEntry);
457
+ if (resultCheck !== null)
458
+ return resultCheck;
459
+ return accept('T-L');
460
+ }
461
+ // ---------------------------------------------------------------------------
462
+ // Application error data schema validation (per-arm)
463
+ // ---------------------------------------------------------------------------
464
+ /**
465
+ * Validate the `data` field of an application error against the
466
+ * per-arm closed schema (additionalProperties: false).
467
+ *
468
+ * 5 arms: HandshakeRejected, DeliveryRejected, DomainError,
469
+ * DeadlineExpired, SnapshotUnavailable.
470
+ *
471
+ * Contract seam: DomainError.data.code (MessagingErrorCode) is validated
472
+ * against MESSAGING_ERROR_CODE_SET, which wraps the generated
473
+ * MESSAGING_ERROR_CODE_VALUES projected from messaging.schema.json —
474
+ * a single truth source (P15), no hand-maintained mirror.
475
+ */
476
+ function validateApplicationErrorData(code, data, method) {
477
+ switch (code) {
478
+ case HANDSHAKE_REJECTED_CODE: {
479
+ // data: { reason: HandshakeRejectReason } — closed
480
+ for (const key of Object.keys(data)) {
481
+ if (!REASON_DATA_KEYS.has(key))
482
+ return close('T-H');
483
+ }
484
+ if (typeof data.reason !== 'string')
485
+ return close('T-H');
486
+ if (!HANDSHAKE_REASONS.has(data.reason))
487
+ return close('T-H');
488
+ return null;
489
+ }
490
+ case DELIVERY_REJECTED_CODE: {
491
+ // data: { reason: DeliveryRejectReason | LifecycleRejectReason } —
492
+ // closed and selected by the in-flight row.
493
+ for (const key of Object.keys(data)) {
494
+ if (!REASON_DATA_KEYS.has(key))
495
+ return close('T-H');
496
+ }
497
+ if (typeof data.reason !== 'string')
498
+ return close('T-H');
499
+ const reasons = DELIVERY_REASONS;
500
+ if (!reasons.has(data.reason))
501
+ return close('T-H');
502
+ return null;
503
+ }
504
+ case DOMAIN_ERROR_CODE: {
505
+ // data: { code: MessagingErrorCode } — closed keys + enum
506
+ // MESSAGING_ERROR_CODE_SET wraps the generated MESSAGING_ERROR_CODE_VALUES
507
+ // (projected from messaging.schema.json, F202 C1).
508
+ for (const key of Object.keys(data)) {
509
+ if (!CODE_DATA_KEYS.has(key))
510
+ return close('T-H');
511
+ }
512
+ if (typeof data.code !== 'string')
513
+ return close('T-H');
514
+ if (!MESSAGING_ERROR_CODE_SET.has(data.code))
515
+ return close('T-H');
516
+ return null;
517
+ }
518
+ case DEADLINE_EXPIRED_CODE: {
519
+ // data: Record<string, never> — must be empty object
520
+ if (Object.keys(data).length !== 0)
521
+ return close('T-H');
522
+ return null;
523
+ }
524
+ case SNAPSHOT_UNAVAILABLE_CODE: {
525
+ // data: { reason: SnapshotUnavailableReason } — closed
526
+ for (const key of Object.keys(data)) {
527
+ if (!REASON_DATA_KEYS.has(key))
528
+ return close('T-H');
529
+ }
530
+ if (typeof data.reason !== 'string')
531
+ return close('T-H');
532
+ if (!SNAPSHOT_REASONS.has(data.reason))
533
+ return close('T-H');
534
+ return null;
535
+ }
536
+ default:
537
+ // Unknown application code — should be unreachable since
538
+ // APPLICATION_CODES was already checked. Defense-in-depth.
539
+ return close('T-H');
540
+ }
541
+ }
542
+ // ---------------------------------------------------------------------------
543
+ // Response result shape validation (per-method)
544
+ // ---------------------------------------------------------------------------
545
+ /**
546
+ * Validate the `result` value of a success response against the
547
+ * correlated in-flight method's expected result shape.
548
+ *
549
+ * CLOSED rows receive full per-method result validation
550
+ * (additionalProperties: false). Any future RESERVED row fails closed.
551
+ *
552
+ * Returns null if valid; DispatchResult (T-H) if invalid.
553
+ */
554
+ function validateResponseResult(result, entry) {
555
+ const method = entry.method;
556
+ const row = WIRE_METHOD_REGISTRY[method];
557
+ // Defense-in-depth for a future RESERVED row without an executable result.
558
+ if (row.leafClosure !== 'CLOSED') {
559
+ return close('T-H');
560
+ }
561
+ if (isMessagingRowMethod(method)) {
562
+ const validated = validateMessagingRowResult(method, result);
563
+ if (!validated.valid)
564
+ return close('T-H');
565
+ if (method === 'messaging.read') {
566
+ const readLimit = entry.requestSnapshot?.readLimit;
567
+ if (readLimit === undefined ||
568
+ !validateMessagingRowInput('messaging.read', {
569
+ subscriptionId: 'request-snapshot',
570
+ limit: readLimit,
571
+ }).valid ||
572
+ validated.value.events.length > readLimit) {
573
+ return close('T-H');
574
+ }
575
+ }
576
+ if (method === 'messaging.snapshot') {
577
+ const snapshotMaxItems = entry.requestSnapshot?.snapshotMaxItems;
578
+ if (snapshotMaxItems === undefined ||
579
+ !validateMessagingRowInput('messaging.snapshot', {
580
+ subscriptionId: 'request-snapshot',
581
+ maxItems: snapshotMaxItems,
582
+ }).valid ||
583
+ validated.value.items.length > snapshotMaxItems) {
584
+ return close('T-H');
585
+ }
586
+ }
587
+ if (method === 'messaging.appendElements') {
588
+ const appendElementIds = entry.requestSnapshot?.appendElementIds;
589
+ if (appendElementIds === undefined)
590
+ return close('T-H');
591
+ const appliedIds = validated.value.appliedElementIds;
592
+ for (const id of appliedIds) {
593
+ if (!appendElementIds.includes(id))
594
+ return close('T-H');
595
+ }
596
+ }
597
+ if (method === 'host.messaging.deliver') {
598
+ const deliveryId = entry.requestSnapshot?.deliveryId;
599
+ if (deliveryId === undefined ||
600
+ !validateMessagingRowResult(method, { deliveryId }).valid ||
601
+ validated.value.deliveryId !== deliveryId) {
602
+ return close('T-H');
603
+ }
604
+ }
605
+ return null;
606
+ }
607
+ switch (method) {
608
+ case 'host.lifecycle.ping': {
609
+ // PingResult: {nonce: string} — additionalProperties: false
610
+ if (result === null || typeof result !== 'object' || Array.isArray(result)) {
611
+ return close('T-H');
612
+ }
613
+ const obj = result;
614
+ // Closed keys: {nonce} only
615
+ for (const key of Object.keys(obj)) {
616
+ if (!PING_RESULT_KEYS.has(key))
617
+ return close('T-H');
618
+ }
619
+ if (typeof obj.nonce !== 'string')
620
+ return close('T-H');
621
+ // Nonce bounds
622
+ const cpLen = [...obj.nonce].length;
623
+ if (cpLen < PING_NONCE_MIN_LENGTH || cpLen > PING_NONCE_MAX_LENGTH) {
624
+ return close('T-H');
625
+ }
626
+ // Cross-frame oracle: nonce byte-equality (REQUIRED for ping).
627
+ // Ping's nonce echo is the fundamental liveness proof — accepting
628
+ // a response without verifying the oracle defeats the purpose.
629
+ // Missing snapshot is a caller bug; fail-closed, not fail-open.
630
+ if (entry.requestSnapshot?.nonce === undefined)
631
+ return close('T-H');
632
+ if (obj.nonce !== entry.requestSnapshot.nonce)
633
+ return close('T-H');
634
+ return null;
635
+ }
636
+ case 'broker.hello': {
637
+ if (!validateSessionBinding(result))
638
+ return close('T-H');
639
+ // SessionBinding is Host-authoritative only for H5–H9. Its four
640
+ // candidate fields are cross-frame echoes, so accepting a structurally
641
+ // valid but different binding would settle the wrong in-flight hello.
642
+ const candidateHello = entry.requestSnapshot?.candidateHello;
643
+ if (candidateHello === undefined)
644
+ return close('T-H');
645
+ if (result.pluginId !== candidateHello.pluginId ||
646
+ result.packageDigest !== candidateHello.packageDigest ||
647
+ result.contractVersion !== candidateHello.contractVersion ||
648
+ result.wireVersion !== candidateHello.wireVersion) {
649
+ return close('T-H');
650
+ }
651
+ return null;
652
+ }
653
+ case 'broker.ready': {
654
+ if (result !== null)
655
+ return close('T-H');
656
+ return null;
657
+ }
658
+ default: {
659
+ // Unreachable for CLOSED rows (all covered above).
660
+ // Defense-in-depth: unknown method in in-flight → T-H.
661
+ return close('T-H');
662
+ }
663
+ }
664
+ }
665
+ // ---------------------------------------------------------------------------
666
+ // Notification sub-classifier (T-J / T-K)
667
+ // ---------------------------------------------------------------------------
668
+ function classifyNotification(_value) { return close('T-K'); }
669
+ // ---------------------------------------------------------------------------
670
+ // Request sub-classifier (T-E / T-F / T-G / T-I / accept)
671
+ // ---------------------------------------------------------------------------
672
+ function classifyRequest(value, id, method, inFlight) {
673
+ // Closed outer keys: {jsonrpc, id, method, params} only
674
+ for (const key of Object.keys(value)) {
675
+ if (!REQUEST_ALLOWED_KEYS.has(key))
676
+ return respondInvalidRequestId(id);
677
+ }
678
+ // Missing params → T-F InvalidRequest
679
+ if (!('params' in value))
680
+ return respondInvalidRequestId(id);
681
+ // params must be an object
682
+ const params = value.params;
683
+ if (params === null || typeof params !== 'object') {
684
+ return respondInvalidParams(id);
685
+ }
686
+ if (Array.isArray(params))
687
+ return respondInvalidParams(id);
688
+ // Method gate: unknown method → T-F MethodNotFound
689
+ if (!isWireMethod(method))
690
+ return respondMethodNotFound(id);
691
+ // Direction gate: notification-only methods must not appear as requests
692
+ const wireMethod = method;
693
+ const row = WIRE_METHOD_REGISTRY[wireMethod];
694
+ if (row.isNotification)
695
+ return respondInvalidRequestId(id);
696
+ // In-flight collision → T-I
697
+ if (inFlight.has(id))
698
+ return close('T-I');
699
+ const paramsObj = params;
700
+ // Closed params keys: {meta, input} only
701
+ for (const key of Object.keys(paramsObj)) {
702
+ if (!PARAMS_ALLOWED_KEYS.has(key))
703
+ return respondInvalidParams(id);
704
+ }
705
+ // Validate params.meta structure
706
+ if (!('meta' in paramsObj))
707
+ return respondInvalidParams(id);
708
+ const meta = paramsObj.meta;
709
+ if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
710
+ return respondInvalidParams(id);
711
+ }
712
+ const metaObj = meta;
713
+ // Closed meta keys: {deadlineUnixMs} only
714
+ for (const key of Object.keys(metaObj)) {
715
+ if (!META_ALLOWED_KEYS.has(key))
716
+ return respondInvalidParamsValue(id);
717
+ }
718
+ // deadlineUnixMs must be positive WireUInt53
719
+ const reqDeadline = metaObj.deadlineUnixMs;
720
+ if (typeof reqDeadline !== 'number' || !isWireUInt53(reqDeadline))
721
+ return respondInvalidParamsValue(id);
722
+ if (reqDeadline === 0)
723
+ return respondInvalidParamsValue(id);
724
+ // Validate params.input exists and is an object
725
+ if (!('input' in paramsObj))
726
+ return respondInvalidParams(id);
727
+ const input = paramsObj.input;
728
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
729
+ return respondInvalidParams(id);
730
+ }
731
+ // Row-specific value validation
732
+ if (row.leafClosure === 'CLOSED') {
733
+ const valueResult = validateClosedRowInput(wireMethod, input, id);
734
+ if (valueResult !== null)
735
+ return valueResult;
736
+ }
737
+ else {
738
+ // Fail closed if a future registry revision introduces a RESERVED row:
739
+ // its input type is `never`, so no legal params value exists.
740
+ return respondInvalidParamsValue(id);
741
+ }
742
+ // Every ready request row is legal at the contract boundary. The standalone
743
+ // shell decides whether a legal request is Host-bound or locally executable;
744
+ // classification must not relabel either family as a rejection or as an
745
+ // unclassified accept.
746
+ if (row.ready) {
747
+ return accept('T-M');
748
+ }
749
+ // Defense-in-depth for a future closed-but-unready plugin-to-host row.
750
+ //
751
+ // Positioned after all envelope/value checks so that:
752
+ // - In-flight collision (T-I) takes precedence (per contract fixtures)
753
+ // - Future RESERVED-row rejection (T-G) takes precedence
754
+ // - Only CLOSED, unready plugin-to-host rows can reach here
755
+ if (row.direction === 'plugin-to-host')
756
+ return respondMethodNotFound(id);
757
+ // All checks passed — valid host-to-plugin CLOSED-row request for dispatch
758
+ return { disposition: null, outcome: 'accept' };
759
+ }
760
+ // ---------------------------------------------------------------------------
761
+ // CLOSED row input validation (T-G detection)
762
+ // ---------------------------------------------------------------------------
763
+ function validateClosedRowInput(method, input, id) {
764
+ if (isMessagingRowMethod(method)) {
765
+ return validateMessagingRowInput(method, input).valid
766
+ ? null
767
+ : respondInvalidParamsValue(id);
768
+ }
769
+ switch (method) {
770
+ case 'broker.hello':
771
+ if (hasHandshakeAuthorityInjection(input))
772
+ return respondHandshakeAuthorityViolation(id);
773
+ return validateCandidateHello(input) ? null : respondInvalidParamsValue(id);
774
+ case 'broker.ready':
775
+ if (hasHandshakeAuthorityInjection(input))
776
+ return respondHandshakeAuthorityViolation(id);
777
+ return validateBrokerReadyParams(input) ? null : respondInvalidParamsValue(id);
778
+ case 'host.lifecycle.ping': {
779
+ // Closed input keys: {nonce} only
780
+ for (const key of Object.keys(input)) {
781
+ if (!PING_INPUT_KEYS.has(key))
782
+ return respondInvalidParamsValue(id);
783
+ }
784
+ // nonce must be string, 1..512 code points
785
+ if (typeof input.nonce !== 'string')
786
+ return respondInvalidParamsValue(id);
787
+ const cpLen = [...input.nonce].length;
788
+ if (cpLen < PING_NONCE_MIN_LENGTH || cpLen > PING_NONCE_MAX_LENGTH) {
789
+ return respondInvalidParamsValue(id);
790
+ }
791
+ return null;
792
+ }
793
+ // in classifyRequest rejects it before reaching this function.
794
+ default:
795
+ return null;
796
+ }
797
+ }
798
+ // ---------------------------------------------------------------------------
799
+ // Main classifier entry point
800
+ // ---------------------------------------------------------------------------
801
+ /**
802
+ * Classify a decoded NDJSON frame into a disposition class.
803
+ *
804
+ * Covers T-C through T-M of the disposition table. T-A and T-B
805
+ * are handled by the NDJSON frame decoder layer.
806
+ *
807
+ * @param frame Decoded frame with raw bytes and parsed value.
808
+ * @param inFlight Map of in-flight request IDs to their entry metadata.
809
+ * @returns The disposition result with class, outcome, and optional error response.
810
+ */
811
+ export function classifyFrame(frame, inFlight) {
812
+ const { raw, value } = frame;
813
+ // ── T-C: canonicality check ──────────────────────────────────────────
814
+ // The T-C predicate covers: whitespace, duplicate keys, non-scalar
815
+ // strings, non-canonical numbers, and BOM-prefixed frames.
816
+ // Byte-equality catches whitespace, duplicate keys, and most non-
817
+ // canonical numbers. Two supplementary measures close the remaining
818
+ // gaps:
819
+ // 1. ignoreBOM:true — keeps BOM visible so it fails byte-equality.
820
+ // 2. containsNonScalarString — lone surrogates roundtrip thru stringify.
821
+ // For protocol-defined WireUInt53 positions (meta.deadlineUnixMs, row-
822
+ // specific input/result leaves including M0-C messaging fields),
823
+ // hasNonCanonicalUInt53Token validates String(n) against the canonical
824
+ // grammar (0|[1-9][0-9]{0,15}) — this catches fractions (1.5),
825
+ // negatives (-1), and V8-canonical exponent form (1e+21) at protocol
826
+ // positions, without rejecting valid numbers in open payloads
827
+ // (e.g. MessageElement.payload).
828
+ //
829
+ // Guard: JSON.stringify and the deep-traversal helpers use recursive
830
+ // descent. A canonical frame nested thousands of levels deep passes
831
+ // V8's iterative JSON.parse but overflows the call stack on stringify.
832
+ // The try-catch ensures classifyFrame never throws — stack overflow
833
+ // is mapped to T-C (close, no response).
834
+ try {
835
+ const rawStr = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(raw);
836
+ const canonical = JSON.stringify(value);
837
+ if (rawStr !== canonical)
838
+ return close('T-C');
839
+ if (containsNonScalarString(value))
840
+ return close('T-C');
841
+ // P1-1: WireUInt53 raw-token grammar — after byte-equality, the raw
842
+ // token at each WireUInt53 position is String(parsedValue). Tokens
843
+ // like "-1", "1.5", or "1e+21" violate 0|[1-9][0-9]{0,15} → T-C.
844
+ if (hasNonCanonicalUInt53Token(value, inFlight))
845
+ return close('T-C');
846
+ }
847
+ catch {
848
+ // Stack overflow from deep nesting, or other canonicality edge case.
849
+ return close('T-C');
850
+ }
851
+ // ── Response candidate detection ─────────────────────────────────────
852
+ const hasMethod = 'method' in value;
853
+ const hasResult = 'result' in value;
854
+ const hasError = 'error' in value;
855
+ if (!hasMethod && (hasResult || hasError)) {
856
+ return classifyResponseCandidate(value, inFlight);
857
+ }
858
+ // ── Structural validity ──────────────────────────────────────────────
859
+ if (value.jsonrpc !== '2.0' || typeof value.method !== 'string') {
860
+ if ('id' in value) {
861
+ const id = validateRequestId(value.id);
862
+ if (id !== null) {
863
+ return respondInvalidRequestId(id);
864
+ }
865
+ }
866
+ return respondInvalidRequestNull();
867
+ }
868
+ const method = value.method;
869
+ // ── Notification vs Request fork ─────────────────────────────────────
870
+ if (!('id' in value)) {
871
+ return classifyNotification(value);
872
+ }
873
+ // ── T-E: profile-invalid id ──────────────────────────────────────────
874
+ const id = validateRequestId(value.id);
875
+ if (id === null)
876
+ return close('T-E');
877
+ // ── Request path ─────────────────────────────────────────────────────
878
+ return classifyRequest(value, id, method, inFlight);
879
+ }
880
+ //# sourceMappingURL=dispatch.js.map