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