@nifrajs/core 1.13.0 → 2.1.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 (233) hide show
  1. package/README.md +37 -1
  2. package/dist/assurance.d.ts +11 -2
  3. package/dist/assurance.d.ts.map +1 -1
  4. package/dist/assurance.js +20 -0
  5. package/dist/assurance.js.map +1 -1
  6. package/dist/capabilities.d.ts +54 -2
  7. package/dist/capabilities.d.ts.map +1 -1
  8. package/dist/capabilities.js +240 -5
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/durable-adapters.d.ts +241 -0
  11. package/dist/durable-adapters.d.ts.map +1 -0
  12. package/dist/durable-adapters.js +697 -0
  13. package/dist/durable-adapters.js.map +1 -0
  14. package/dist/durable-execution.d.ts +349 -0
  15. package/dist/durable-execution.d.ts.map +1 -0
  16. package/dist/durable-execution.js +1173 -0
  17. package/dist/durable-execution.js.map +1 -0
  18. package/dist/effect-ledger.d.ts +9 -0
  19. package/dist/effect-ledger.d.ts.map +1 -0
  20. package/dist/effect-ledger.js +8 -0
  21. package/dist/effect-ledger.js.map +1 -0
  22. package/dist/effect-lifecycle.d.ts +42 -0
  23. package/dist/effect-lifecycle.d.ts.map +1 -0
  24. package/dist/effect-lifecycle.js +66 -0
  25. package/dist/effect-lifecycle.js.map +1 -0
  26. package/dist/effect-scope.d.ts +75 -0
  27. package/dist/effect-scope.d.ts.map +1 -0
  28. package/dist/effect-scope.js +177 -0
  29. package/dist/effect-scope.js.map +1 -0
  30. package/dist/idempotency-plugin.d.ts +13 -0
  31. package/dist/idempotency-plugin.d.ts.map +1 -0
  32. package/dist/idempotency-plugin.js +16 -0
  33. package/dist/idempotency-plugin.js.map +1 -0
  34. package/dist/index.d.ts +6 -35
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +6 -31
  37. package/dist/index.js.map +1 -1
  38. package/dist/internal/capability-runtime.d.ts +62 -1
  39. package/dist/internal/capability-runtime.d.ts.map +1 -1
  40. package/dist/internal/capability-runtime.js +11 -2
  41. package/dist/internal/capability-runtime.js.map +1 -1
  42. package/dist/internal/effect-execution.d.ts +16 -0
  43. package/dist/internal/effect-execution.d.ts.map +1 -0
  44. package/dist/internal/effect-execution.js +50 -0
  45. package/dist/internal/effect-execution.js.map +1 -0
  46. package/dist/internal/safe-equal.d.ts +20 -0
  47. package/dist/internal/safe-equal.d.ts.map +1 -0
  48. package/dist/internal/safe-equal.js +31 -0
  49. package/dist/internal/safe-equal.js.map +1 -0
  50. package/dist/ledger.d.ts +14 -6
  51. package/dist/ledger.d.ts.map +1 -1
  52. package/dist/ledger.js +31 -5
  53. package/dist/ledger.js.map +1 -1
  54. package/dist/mcp.d.ts +7 -0
  55. package/dist/mcp.d.ts.map +1 -0
  56. package/dist/mcp.js +56 -0
  57. package/dist/mcp.js.map +1 -0
  58. package/dist/node-direct.d.ts +5 -0
  59. package/dist/node-direct.d.ts.map +1 -0
  60. package/dist/node-direct.js +27 -0
  61. package/dist/node-direct.js.map +1 -0
  62. package/dist/reconciliation-worker.d.ts +108 -0
  63. package/dist/reconciliation-worker.d.ts.map +1 -0
  64. package/dist/reconciliation-worker.js +239 -0
  65. package/dist/reconciliation-worker.js.map +1 -0
  66. package/dist/reflection.d.ts +5 -0
  67. package/dist/reflection.d.ts.map +1 -1
  68. package/dist/reflection.js +2 -0
  69. package/dist/reflection.js.map +1 -1
  70. package/dist/server/client-ip.d.ts +35 -0
  71. package/dist/server/client-ip.d.ts.map +1 -0
  72. package/dist/server/client-ip.js +42 -0
  73. package/dist/server/client-ip.js.map +1 -0
  74. package/dist/server/context.d.ts +47 -2
  75. package/dist/server/context.d.ts.map +1 -1
  76. package/dist/server/contract.d.ts +14 -4
  77. package/dist/server/contract.d.ts.map +1 -1
  78. package/dist/server/contract.js +8 -2
  79. package/dist/server/contract.js.map +1 -1
  80. package/dist/server/http.d.ts +14 -0
  81. package/dist/server/http.d.ts.map +1 -0
  82. package/dist/server/http.js +45 -0
  83. package/dist/server/http.js.map +1 -0
  84. package/dist/server/idempotency-lane.d.ts +45 -0
  85. package/dist/server/idempotency-lane.d.ts.map +1 -0
  86. package/dist/server/idempotency-lane.js +205 -0
  87. package/dist/server/idempotency-lane.js.map +1 -0
  88. package/dist/server/install.d.ts +21 -0
  89. package/dist/server/install.d.ts.map +1 -0
  90. package/dist/server/install.js +21 -0
  91. package/dist/server/install.js.map +1 -0
  92. package/dist/server/ledger-lane.d.ts +53 -0
  93. package/dist/server/ledger-lane.d.ts.map +1 -0
  94. package/dist/server/ledger-lane.js +82 -0
  95. package/dist/server/ledger-lane.js.map +1 -0
  96. package/dist/server/mcp-hook.d.ts +28 -0
  97. package/dist/server/mcp-hook.d.ts.map +1 -0
  98. package/dist/server/mcp-hook.js +2 -0
  99. package/dist/server/mcp-hook.js.map +1 -0
  100. package/dist/server/node-outcome-hook.d.ts +9 -0
  101. package/dist/server/node-outcome-hook.d.ts.map +1 -0
  102. package/dist/server/node-outcome-hook.js +2 -0
  103. package/dist/server/node-outcome-hook.js.map +1 -0
  104. package/dist/server/node-outcome.d.ts +37 -0
  105. package/dist/server/node-outcome.d.ts.map +1 -0
  106. package/dist/server/node-outcome.js +75 -0
  107. package/dist/server/node-outcome.js.map +1 -0
  108. package/dist/server/plugin.d.ts +78 -0
  109. package/dist/server/plugin.d.ts.map +1 -0
  110. package/dist/server/plugin.js +57 -0
  111. package/dist/server/plugin.js.map +1 -0
  112. package/dist/server/query.d.ts +18 -0
  113. package/dist/server/query.d.ts.map +1 -0
  114. package/dist/server/query.js +102 -0
  115. package/dist/server/query.js.map +1 -0
  116. package/dist/server/registry.d.ts +31 -6
  117. package/dist/server/registry.d.ts.map +1 -1
  118. package/dist/server/request-context.d.ts +51 -0
  119. package/dist/server/request-context.d.ts.map +1 -0
  120. package/dist/server/request-context.js +184 -0
  121. package/dist/server/request-context.js.map +1 -0
  122. package/dist/server/respond.d.ts +17 -0
  123. package/dist/server/respond.d.ts.map +1 -0
  124. package/dist/server/respond.js +81 -0
  125. package/dist/server/respond.js.map +1 -0
  126. package/dist/server/runtime-core.d.ts +36 -0
  127. package/dist/server/runtime-core.d.ts.map +1 -0
  128. package/dist/server/runtime-core.js +51 -0
  129. package/dist/server/runtime-core.js.map +1 -0
  130. package/dist/server/server-types.d.ts +203 -0
  131. package/dist/server/server-types.d.ts.map +1 -0
  132. package/dist/server/server-types.js +2 -0
  133. package/dist/server/server-types.js.map +1 -0
  134. package/dist/server/server.d.ts +119 -345
  135. package/dist/server/server.d.ts.map +1 -1
  136. package/dist/server/server.js +247 -833
  137. package/dist/server/server.js.map +1 -1
  138. package/dist/server/sse-hook.d.ts +10 -0
  139. package/dist/server/sse-hook.d.ts.map +1 -0
  140. package/dist/server/sse-hook.js +2 -0
  141. package/dist/server/sse-hook.js.map +1 -0
  142. package/dist/server/sse.d.ts +7 -23
  143. package/dist/server/sse.d.ts.map +1 -1
  144. package/dist/server/sse.js +18 -0
  145. package/dist/server/sse.js.map +1 -1
  146. package/dist/server/transport-lane.d.ts +28 -0
  147. package/dist/server/transport-lane.d.ts.map +1 -0
  148. package/dist/server/transport-lane.js +106 -0
  149. package/dist/server/transport-lane.js.map +1 -0
  150. package/dist/server/websocket.d.ts +23 -1
  151. package/dist/server/websocket.d.ts.map +1 -1
  152. package/dist/server/websocket.js +10 -1
  153. package/dist/server/websocket.js.map +1 -1
  154. package/dist/server/ws-hook.d.ts +8 -11
  155. package/dist/server/ws-hook.d.ts.map +1 -1
  156. package/dist/server/ws-hook.js +7 -14
  157. package/dist/server/ws-hook.js.map +1 -1
  158. package/dist/server.d.ts +21 -0
  159. package/dist/server.d.ts.map +1 -0
  160. package/dist/server.js +16 -0
  161. package/dist/server.js.map +1 -0
  162. package/dist/transport-codec-rich.d.ts +8 -0
  163. package/dist/transport-codec-rich.d.ts.map +1 -0
  164. package/dist/transport-codec-rich.js +14 -0
  165. package/dist/transport-codec-rich.js.map +1 -0
  166. package/dist/transport-codec.d.ts +31 -0
  167. package/dist/transport-codec.d.ts.map +1 -0
  168. package/dist/transport-codec.js +190 -0
  169. package/dist/transport-codec.js.map +1 -0
  170. package/dist/transport-plugin.d.ts +3 -0
  171. package/dist/transport-plugin.d.ts.map +1 -0
  172. package/dist/transport-plugin.js +3 -0
  173. package/dist/transport-plugin.js.map +1 -0
  174. package/dist/wire.d.ts +58 -0
  175. package/dist/wire.d.ts.map +1 -0
  176. package/dist/wire.js +445 -0
  177. package/dist/wire.js.map +1 -0
  178. package/dist/ws.d.ts +16 -7
  179. package/dist/ws.d.ts.map +1 -1
  180. package/dist/ws.js +25 -10
  181. package/dist/ws.js.map +1 -1
  182. package/package.json +122 -6
  183. package/src/assurance.ts +36 -1
  184. package/src/capabilities.ts +306 -5
  185. package/src/durable-adapters.ts +1039 -0
  186. package/src/durable-execution.ts +1676 -0
  187. package/src/effect-ledger.ts +9 -0
  188. package/src/effect-lifecycle.ts +120 -0
  189. package/src/effect-scope.ts +267 -0
  190. package/src/idempotency-plugin.ts +17 -0
  191. package/src/index.ts +6 -133
  192. package/src/internal/capability-runtime.ts +87 -1
  193. package/src/internal/effect-execution.ts +70 -0
  194. package/src/internal/safe-equal.ts +27 -0
  195. package/src/ledger.ts +45 -11
  196. package/src/mcp.ts +65 -0
  197. package/src/node-direct.ts +37 -0
  198. package/src/reconciliation-worker.ts +372 -0
  199. package/src/reflection.ts +7 -0
  200. package/src/server/client-ip.ts +60 -0
  201. package/src/server/context.ts +50 -2
  202. package/src/server/contract.ts +26 -7
  203. package/src/server/http.ts +58 -0
  204. package/src/server/idempotency-lane.ts +321 -0
  205. package/src/server/install.ts +28 -0
  206. package/src/server/ledger-lane.ts +125 -0
  207. package/src/server/mcp-hook.ts +41 -0
  208. package/src/server/node-outcome-hook.ts +9 -0
  209. package/src/server/node-outcome.ts +112 -0
  210. package/src/server/plugin.ts +92 -0
  211. package/src/server/query.ts +119 -0
  212. package/src/server/registry.ts +36 -6
  213. package/src/server/request-context.ts +271 -0
  214. package/src/server/respond.ts +94 -0
  215. package/src/server/runtime-core.ts +74 -0
  216. package/src/server/server-types.ts +204 -0
  217. package/src/server/server.ts +448 -1314
  218. package/src/server/sse-hook.ts +14 -0
  219. package/src/server/sse.ts +26 -0
  220. package/src/server/transport-lane.ts +141 -0
  221. package/src/server/websocket.ts +35 -1
  222. package/src/server/ws-hook.ts +8 -19
  223. package/src/server.ts +92 -0
  224. package/src/transport-codec-rich.ts +20 -0
  225. package/src/transport-codec.ts +250 -0
  226. package/src/transport-plugin.ts +2 -0
  227. package/src/wire.ts +474 -0
  228. package/src/ws.ts +33 -10
  229. package/dist/invariants.d.ts +0 -70
  230. package/dist/invariants.d.ts.map +0 -1
  231. package/dist/invariants.js +0 -487
  232. package/dist/invariants.js.map +0 -1
  233. package/src/invariants.ts +0 -604
package/src/wire.ts ADDED
@@ -0,0 +1,474 @@
1
+ /**
2
+ * Rich-type wire codec: round-trips values that plain JSON silently drops or corrupts, across any
3
+ * JSON transport (RPC bodies, loader payloads, WebSocket frames).
4
+ *
5
+ * `JSON.stringify` turns `undefined` into a missing key, `Date` into a lossy string, `NaN`/`Infinity`
6
+ * into `null`, loses `-0`, and throws on `BigInt` - and it has no notion of `Map`, `Set`, `RegExp`,
7
+ * `URL`, `ArrayBuffer`, or typed arrays. It also flattens a shared reference into two copies and throws
8
+ * on a cycle. A typed client that infers `Date` from the server type then receives a `string` at runtime
9
+ * is exactly the drift this framework exists to remove. `encode` maps a value to a JSON-safe structure
10
+ * that `decode` reconstructs exactly - including cycles and preserved reference identity; `stringify` /
11
+ * `parse` are the drop-in `JSON.*` equivalents.
12
+ *
13
+ * The wire form is `{ r, n }`: `r` is the root (an inline value or a `ref`), `n` is a flat table of
14
+ * container/rich nodes addressed by index. Because every object is interned in `n` and referenced by
15
+ * index, a shared object is encoded once (and decodes to a single shared instance), and a cycle is a
16
+ * back-reference rather than infinite recursion. Functions and symbols are rejected, never silently
17
+ * dropped. Decoding validates every tag and index, so a malformed payload throws {@link WireDecodeError}
18
+ * rather than producing a corrupt value.
19
+ *
20
+ * Not covered by design (matches `JSON`): class identity (a class instance decodes to a plain object
21
+ * carrying its own enumerable properties), non-enumerable and symbol keys, and getters (read once).
22
+ */
23
+
24
+ /** Discriminator key on a tagged wire node. Never collides with user data: user objects live under a
25
+ * node's `v` field, so their own keys - including one literally named `$w` - are never re-interpreted. */
26
+ const TAG = "$w"
27
+
28
+ /** The JSON-safe encoded form produced by {@link encode} and consumed by {@link decode}. */
29
+ export interface Wire {
30
+ /** The root value: an inline primitive/scalar, or a `{ [TAG]: "ref", i }` into {@link Wire.n}. */
31
+ readonly r: unknown
32
+ /** Interned container and rich-type nodes, addressed by index from `ref`s. */
33
+ readonly n: readonly unknown[]
34
+ }
35
+
36
+ /** Resource limits applied while reconstructing transport-controlled wire data. */
37
+ export interface WireDecodeLimits {
38
+ /** Maximum number of interned nodes. Default 10,000. */
39
+ readonly maxNodes?: number
40
+ /** Maximum nesting depth across containers and references. Default 256. */
41
+ readonly maxDepth?: number
42
+ /** Maximum total object/array/map/set entries visited. Default 100,000. */
43
+ readonly maxCollectionEntries?: number
44
+ /** Maximum UTF-8 string plus decoded binary bytes materialized. Default 16 MiB. */
45
+ readonly maxDecodedBytes?: number
46
+ }
47
+
48
+ export const DEFAULT_WIRE_DECODE_LIMITS: Readonly<Required<WireDecodeLimits>> = Object.freeze({
49
+ maxNodes: 10_000,
50
+ maxDepth: 256,
51
+ maxCollectionEntries: 100_000,
52
+ maxDecodedBytes: 16 * 1024 * 1024,
53
+ })
54
+
55
+ /** Thrown by {@link encode} for a value it will not encode (a function or a symbol). */
56
+ export class WireEncodeError extends TypeError {
57
+ constructor(message: string) {
58
+ super(message)
59
+ this.name = "WireEncodeError"
60
+ }
61
+ }
62
+
63
+ /** Thrown by {@link decode} for a wire value carrying an unknown tag, a bad index, or malformed shape. */
64
+ export class WireDecodeError extends TypeError {
65
+ constructor(message: string) {
66
+ super(message)
67
+ this.name = "WireDecodeError"
68
+ }
69
+ }
70
+
71
+ function isRecord(value: unknown): value is Record<string, unknown> {
72
+ return value !== null && typeof value === "object" && !Array.isArray(value)
73
+ }
74
+
75
+ function decodeLimit(value: number | undefined, fallback: number, label: string): number {
76
+ const limit = value ?? fallback
77
+ if (!Number.isSafeInteger(limit) || limit < 0) {
78
+ throw new RangeError(`wire decode ${label} must be a non-negative safe integer`)
79
+ }
80
+ return limit
81
+ }
82
+
83
+ function utf8ByteLength(value: string, stopAfter = Number.MAX_SAFE_INTEGER): number {
84
+ let bytes = 0
85
+ for (let index = 0; index < value.length; index++) {
86
+ const code = value.charCodeAt(index)
87
+ if (code < 0x80) bytes++
88
+ else if (code < 0x800) bytes += 2
89
+ else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
90
+ const next = value.charCodeAt(index + 1)
91
+ if (next >= 0xdc00 && next <= 0xdfff) {
92
+ bytes += 4
93
+ index++
94
+ } else bytes += 3
95
+ } else bytes += 3
96
+ if (bytes > stopAfter) return bytes
97
+ }
98
+ return bytes
99
+ }
100
+
101
+ /** The typed-array kinds the codec round-trips, by constructor name. */
102
+ const TYPED_ARRAYS = {
103
+ Int8Array,
104
+ Uint8Array,
105
+ Uint8ClampedArray,
106
+ Int16Array,
107
+ Uint16Array,
108
+ Int32Array,
109
+ Uint32Array,
110
+ Float32Array,
111
+ Float64Array,
112
+ BigInt64Array,
113
+ BigUint64Array,
114
+ } as const
115
+ type TypedArrayName = keyof typeof TYPED_ARRAYS
116
+ type TypedArray = InstanceType<(typeof TYPED_ARRAYS)[TypedArrayName]>
117
+
118
+ function defineEnumerableOwn(target: object, key: string, value: unknown): void {
119
+ Object.defineProperty(target, key, {
120
+ value,
121
+ enumerable: true,
122
+ configurable: true,
123
+ writable: true,
124
+ })
125
+ }
126
+
127
+ // ─── encode ─────────────────────────────────────────────────────────────────
128
+
129
+ /** Encode any supported value into a JSON-safe {@link Wire} form. */
130
+ export function encode(value: unknown): Wire {
131
+ const nodes: unknown[] = []
132
+ const interned = new Map<object, number>()
133
+
134
+ /** Encode a value to its inline wire form, interning objects into `nodes` and returning a `ref`. */
135
+ const inline = (v: unknown): unknown => {
136
+ if (v === undefined) return { [TAG]: "undef" }
137
+ if (v === null) return null
138
+
139
+ switch (typeof v) {
140
+ case "string":
141
+ case "boolean":
142
+ return v
143
+ case "number":
144
+ if (Object.is(v, -0)) return { [TAG]: "num", v: "-0" }
145
+ if (Number.isNaN(v)) return { [TAG]: "num", v: "NaN" }
146
+ if (v === Infinity) return { [TAG]: "num", v: "Infinity" }
147
+ if (v === -Infinity) return { [TAG]: "num", v: "-Infinity" }
148
+ return v // a finite, signed-zero-free number is already JSON-safe
149
+ case "bigint":
150
+ return { [TAG]: "bigint", v: v.toString() }
151
+ case "function":
152
+ case "symbol":
153
+ throw new WireEncodeError(`cannot encode a ${typeof v}`)
154
+ }
155
+
156
+ const obj = v as object
157
+ const existing = interned.get(obj)
158
+ if (existing !== undefined) return { [TAG]: "ref", i: existing }
159
+
160
+ // Reserve this object's index BEFORE encoding its body, so a self- or mutual-reference inside the
161
+ // body resolves to a `ref` here instead of recursing forever.
162
+ const index = nodes.length
163
+ interned.set(obj, index)
164
+ nodes.push(0) // placeholder, overwritten below
165
+ nodes[index] = body(v, inline)
166
+ return { [TAG]: "ref", i: index }
167
+ }
168
+
169
+ return { r: inline(value), n: nodes }
170
+ }
171
+
172
+ /** Encode the interned body of one object into its tagged node. */
173
+ function body(v: unknown, inline: (x: unknown) => unknown): unknown {
174
+ if (v instanceof Date) {
175
+ const time = v.getTime()
176
+ return { [TAG]: "date", v: Number.isNaN(time) ? null : v.toISOString() }
177
+ }
178
+ if (v instanceof RegExp) return { [TAG]: "regexp", v: [v.source, v.flags] }
179
+ if (v instanceof URL) return { [TAG]: "url", v: v.href }
180
+ if (v instanceof ArrayBuffer) return { [TAG]: "buf", v: bytesToBase64(new Uint8Array(v)) }
181
+ if (v instanceof DataView) {
182
+ return { [TAG]: "dv", v: bytesToBase64(new Uint8Array(v.buffer, v.byteOffset, v.byteLength)) }
183
+ }
184
+ if (ArrayBuffer.isView(v)) {
185
+ const view = v as TypedArray
186
+ const kind = view.constructor.name
187
+ if (!(kind in TYPED_ARRAYS)) throw new WireEncodeError(`cannot encode typed array ${kind}`)
188
+ return {
189
+ [TAG]: "ta",
190
+ k: kind,
191
+ v: bytesToBase64(new Uint8Array(view.buffer, view.byteOffset, view.byteLength)),
192
+ }
193
+ }
194
+ if (v instanceof Map) {
195
+ return { [TAG]: "map", v: Array.from(v, ([k, val]) => [inline(k), inline(val)]) }
196
+ }
197
+ if (v instanceof Set) return { [TAG]: "set", v: Array.from(v, (item) => inline(item)) }
198
+ if (Array.isArray(v)) return { [TAG]: "arr", v: v.map((item) => inline(item)) }
199
+
200
+ const record = v as Record<string, unknown>
201
+ const out: Record<string, unknown> = {}
202
+ for (const key of Object.keys(record)) defineEnumerableOwn(out, key, inline(record[key]))
203
+ return { [TAG]: "obj", v: out }
204
+ }
205
+
206
+ // ─── decode ─────────────────────────────────────────────────────────────────
207
+
208
+ /** Reconstruct the original value from a {@link Wire} form produced by {@link encode}. */
209
+ export function decode(wire: Wire, limits: WireDecodeLimits = {}): unknown {
210
+ if (!isRecord(wire) || !Object.hasOwn(wire, "r") || !Array.isArray(wire.n)) {
211
+ throw new WireDecodeError("not a wire value: expected { r, n }")
212
+ }
213
+ const nodes = wire.n
214
+ const maxNodes = decodeLimit(limits.maxNodes, DEFAULT_WIRE_DECODE_LIMITS.maxNodes, "maxNodes")
215
+ const maxDepth = decodeLimit(limits.maxDepth, DEFAULT_WIRE_DECODE_LIMITS.maxDepth, "maxDepth")
216
+ const maxCollectionEntries = decodeLimit(
217
+ limits.maxCollectionEntries,
218
+ DEFAULT_WIRE_DECODE_LIMITS.maxCollectionEntries,
219
+ "maxCollectionEntries",
220
+ )
221
+ const maxDecodedBytes = decodeLimit(
222
+ limits.maxDecodedBytes,
223
+ DEFAULT_WIRE_DECODE_LIMITS.maxDecodedBytes,
224
+ "maxDecodedBytes",
225
+ )
226
+ if (nodes.length > maxNodes) throw new WireDecodeError("wire node limit exceeded")
227
+ const built = new Map<number, unknown>()
228
+ let collectionEntries = 0
229
+ let decodedBytes = 0
230
+
231
+ const enter = (depth: number): void => {
232
+ if (depth > maxDepth) throw new WireDecodeError("wire depth limit exceeded")
233
+ }
234
+ const chargeEntries = (count: number): void => {
235
+ collectionEntries += count
236
+ if (!Number.isSafeInteger(collectionEntries) || collectionEntries > maxCollectionEntries) {
237
+ throw new WireDecodeError("wire collection entry limit exceeded")
238
+ }
239
+ }
240
+ const chargeBytes = (count: number): void => {
241
+ decodedBytes += count
242
+ if (!Number.isSafeInteger(decodedBytes) || decodedBytes > maxDecodedBytes) {
243
+ throw new WireDecodeError("wire decoded byte limit exceeded")
244
+ }
245
+ }
246
+ const chargeString = (value: string): void =>
247
+ chargeBytes(utf8ByteLength(value, maxDecodedBytes - decodedBytes))
248
+
249
+ const inline = (w: unknown, depth: number): unknown => {
250
+ enter(depth)
251
+ if (w === null || typeof w === "boolean") return w
252
+ if (typeof w === "string") {
253
+ chargeString(w)
254
+ return w
255
+ }
256
+ if (typeof w === "number") {
257
+ if (!Number.isFinite(w) || Object.is(w, -0)) {
258
+ throw new WireDecodeError("malformed wire: non-JSON number inline")
259
+ }
260
+ return w
261
+ }
262
+ if (!isRecord(w)) throw new WireDecodeError("malformed wire: invalid inline value")
263
+ const record = w
264
+ const tag = record[TAG]
265
+ if (typeof tag !== "string")
266
+ throw new WireDecodeError("malformed wire: object without a tag inline")
267
+ switch (tag) {
268
+ case "undef":
269
+ return undefined
270
+ case "num": {
271
+ const v = record.v
272
+ if (v === "-0") return -0
273
+ if (v === "NaN") return NaN
274
+ if (v === "Infinity") return Infinity
275
+ if (v === "-Infinity") return -Infinity
276
+ throw new WireDecodeError(`malformed num token: ${String(v)}`)
277
+ }
278
+ case "bigint": {
279
+ if (typeof record.v !== "string" || !/^-?(?:0|[1-9][0-9]*)$/u.test(record.v)) {
280
+ throw new WireDecodeError("malformed bigint token")
281
+ }
282
+ chargeString(record.v)
283
+ return BigInt(record.v)
284
+ }
285
+ case "ref":
286
+ return resolve(record.i, depth)
287
+ default:
288
+ throw new WireDecodeError(`unexpected inline tag: ${tag}`)
289
+ }
290
+ }
291
+
292
+ const resolve = (raw: unknown, depth: number): unknown => {
293
+ enter(depth)
294
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0 || raw >= nodes.length) {
295
+ throw new WireDecodeError(`ref out of range: ${String(raw)}`)
296
+ }
297
+ const index = raw
298
+ const cached = built.get(index)
299
+ if (cached !== undefined || built.has(index)) return cached
300
+
301
+ const node = nodes[index]
302
+ if (!isRecord(node)) throw new WireDecodeError(`malformed node at ${index}`)
303
+ const record = node
304
+ const tag = record[TAG]
305
+
306
+ switch (tag) {
307
+ case "obj": {
308
+ if (!isRecord(record.v)) throw new WireDecodeError(`malformed obj node at ${index}`)
309
+ const out: Record<string, unknown> = {}
310
+ built.set(index, out) // publish the shell first so cycles resolve to it
311
+ const src = record.v
312
+ const keys = Object.keys(src)
313
+ chargeEntries(keys.length)
314
+ for (const key of keys) {
315
+ chargeString(key)
316
+ defineEnumerableOwn(out, key, inline(src[key], depth + 1))
317
+ }
318
+ return out
319
+ }
320
+ case "arr": {
321
+ if (!Array.isArray(record.v)) throw new WireDecodeError(`malformed arr node at ${index}`)
322
+ const out: unknown[] = []
323
+ built.set(index, out)
324
+ chargeEntries(record.v.length)
325
+ for (const item of record.v) out.push(inline(item, depth + 1))
326
+ return out
327
+ }
328
+ case "map": {
329
+ if (!Array.isArray(record.v)) throw new WireDecodeError(`malformed map node at ${index}`)
330
+ const out = new Map<unknown, unknown>()
331
+ built.set(index, out)
332
+ chargeEntries(record.v.length)
333
+ for (const entry of record.v) {
334
+ if (!Array.isArray(entry) || entry.length !== 2) {
335
+ throw new WireDecodeError(`malformed map entry at ${index}`)
336
+ }
337
+ out.set(inline(entry[0], depth + 1), inline(entry[1], depth + 1))
338
+ }
339
+ return out
340
+ }
341
+ case "set": {
342
+ if (!Array.isArray(record.v)) throw new WireDecodeError(`malformed set node at ${index}`)
343
+ const out = new Set<unknown>()
344
+ built.set(index, out)
345
+ chargeEntries(record.v.length)
346
+ for (const item of record.v) out.add(inline(item, depth + 1))
347
+ return out
348
+ }
349
+ case "date": {
350
+ if (record.v !== null && typeof record.v !== "string") {
351
+ throw new WireDecodeError(`malformed date node at ${index}`)
352
+ }
353
+ if (record.v !== null) chargeString(record.v)
354
+ const out = record.v === null ? new Date(NaN) : new Date(record.v)
355
+ if (record.v !== null && (Number.isNaN(out.getTime()) || out.toISOString() !== record.v)) {
356
+ throw new WireDecodeError(`malformed date node at ${index}`)
357
+ }
358
+ built.set(index, out)
359
+ return out
360
+ }
361
+ case "regexp": {
362
+ if (
363
+ !Array.isArray(record.v) ||
364
+ record.v.length !== 2 ||
365
+ typeof record.v[0] !== "string" ||
366
+ typeof record.v[1] !== "string"
367
+ ) {
368
+ throw new WireDecodeError(`malformed regexp node at ${index}`)
369
+ }
370
+ chargeString(record.v[0])
371
+ chargeString(record.v[1])
372
+ let out: RegExp
373
+ try {
374
+ out = new RegExp(record.v[0], record.v[1])
375
+ } catch {
376
+ throw new WireDecodeError(`malformed regexp node at ${index}`)
377
+ }
378
+ built.set(index, out)
379
+ return out
380
+ }
381
+ case "url": {
382
+ if (typeof record.v !== "string") {
383
+ throw new WireDecodeError(`malformed url node at ${index}`)
384
+ }
385
+ chargeString(record.v)
386
+ let out: URL
387
+ try {
388
+ out = new URL(record.v)
389
+ } catch {
390
+ throw new WireDecodeError(`malformed url node at ${index}`)
391
+ }
392
+ built.set(index, out)
393
+ return out
394
+ }
395
+ case "buf": {
396
+ const out = base64ToBytes(record.v, index, chargeBytes).buffer
397
+ built.set(index, out)
398
+ return out
399
+ }
400
+ case "dv": {
401
+ const out = new DataView(base64ToBytes(record.v, index, chargeBytes).buffer)
402
+ built.set(index, out)
403
+ return out
404
+ }
405
+ case "ta": {
406
+ const kind = record.k
407
+ if (typeof kind !== "string" || !(kind in TYPED_ARRAYS)) {
408
+ throw new WireDecodeError(`unknown typed array: ${String(kind)}`)
409
+ }
410
+ const Ctor = TYPED_ARRAYS[kind as TypedArrayName]
411
+ const bytes = base64ToBytes(record.v, index, chargeBytes)
412
+ if (bytes.byteLength % Ctor.BYTES_PER_ELEMENT !== 0) {
413
+ throw new WireDecodeError(`misaligned bytes for ${kind}`)
414
+ }
415
+ // `base64ToBytes` always allocates a fresh, non-shared ArrayBuffer, so this narrowing is sound.
416
+ const out = new Ctor(
417
+ bytes.buffer as ArrayBuffer,
418
+ 0,
419
+ bytes.byteLength / Ctor.BYTES_PER_ELEMENT,
420
+ )
421
+ built.set(index, out)
422
+ return out
423
+ }
424
+ default:
425
+ throw new WireDecodeError(`unknown wire tag: ${String(tag)}`)
426
+ }
427
+ }
428
+
429
+ return inline(wire.r, 0)
430
+ }
431
+
432
+ /** `encode` + `JSON.stringify` in one call - the rich-type equivalent of `JSON.stringify`. */
433
+ export function stringify(value: unknown): string {
434
+ return JSON.stringify(encode(value))
435
+ }
436
+
437
+ /** `JSON.parse` + `decode` in one call - the rich-type equivalent of `JSON.parse`. */
438
+ export function parse(text: string, limits: WireDecodeLimits = {}): unknown {
439
+ return decode(JSON.parse(text) as Wire, limits)
440
+ }
441
+
442
+ // ─── base64 (portable across Bun / browser / Workers / Node / Deno) ───────────
443
+
444
+ function bytesToBase64(bytes: Uint8Array): string {
445
+ let binary = ""
446
+ const CHUNK = 0x8000 // chunk so String.fromCharCode(...) never overflows the call stack
447
+ for (let i = 0; i < bytes.length; i += CHUNK)
448
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK))
449
+ return btoa(binary)
450
+ }
451
+
452
+ function base64ToBytes(
453
+ base64: unknown,
454
+ index: number,
455
+ reserve: (decodedBytes: number) => void,
456
+ ): Uint8Array {
457
+ if (typeof base64 !== "string" || base64.length % 4 !== 0) {
458
+ throw new WireDecodeError(`malformed base64 at ${index}`)
459
+ }
460
+ const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0
461
+ reserve((base64.length / 4) * 3 - padding)
462
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(base64)) {
463
+ throw new WireDecodeError(`malformed base64 at ${index}`)
464
+ }
465
+ let binary: string
466
+ try {
467
+ binary = atob(base64)
468
+ } catch {
469
+ throw new WireDecodeError(`malformed base64 at ${index}`)
470
+ }
471
+ const bytes = new Uint8Array(binary.length)
472
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
473
+ return bytes
474
+ }
package/src/ws.ts CHANGED
@@ -1,33 +1,56 @@
1
1
  /**
2
- * WebSocket runtime for `@nifrajs/core` — import once at your server entry to enable `app.ws()`:
2
+ * The opt-in WebSocket runtime plugin.
3
3
  *
4
- * import "@nifrajs/core/ws"
4
+ * `.use(websocket())` enables `app.ws()`:
5
5
  *
6
- * const app = server().ws("/chat", { message: (ws, data) => ws.send(data) })
6
+ * import { websocket } from "@nifrajs/core/ws"
7
7
  *
8
- * Why a subpath: an app that never uses WebSockets shouldn't ship the WS dispatcher, pub/sub
9
- * registry, and Bun socket plumbing in its server bundle (~1.5 KB gzipped on the minimal-app size
10
- * benchmark). Importing this module registers the runtime with the core server (idempotent);
11
- * calling `app.ws()` without it fails loud at boot with a `WS_RUNTIME_MISSING` error pointing here.
8
+ * const app = server().use(websocket()).ws("/chat", { message: (ws, data) => ws.send(data) })
9
+ *
10
+ * Without it, `app.ws()` throws at registration (fail-closed), so an app that never uses WebSockets
11
+ * never evaluates or bundles the WS dispatcher, pub/sub registry, and Bun socket plumbing (~1.5 KB
12
+ * gzipped on the minimal-app size benchmark). The runtime installs on that server instance only -
13
+ * the same `.use()` install seam as `mcp()` / `streaming()` / `idempotency()`, not a process-global
14
+ * or a side-effect import.
12
15
  *
13
16
  * Serving adapters that wire standard sockets themselves (`@nifrajs/workers`) import
14
17
  * `attachWebSocket` / `TopicRegistry` from here too.
15
18
  */
16
19
 
20
+ import { INSTALL_WS } from "./server/install.ts"
21
+ import type { IdentityPlugin } from "./server/plugin.ts"
22
+ import type { AnyServer } from "./server/server.ts"
17
23
  import {
18
24
  attachWebSocket,
19
25
  TopicRegistry,
20
26
  wrapWebSocketMessageValidation,
21
27
  } from "./server/websocket.ts"
22
28
  import { createBunWsHandlers } from "./server/ws-bun.ts"
23
- import { setWsRuntime } from "./server/ws-hook.ts"
29
+ import type { WsRuntime } from "./server/ws-hook.ts"
24
30
 
25
- setWsRuntime({
31
+ const WS_RUNTIME: WsRuntime = {
26
32
  wrapHandler: wrapWebSocketMessageValidation,
27
33
  createTopics: () => new TopicRegistry(),
28
34
  bunHandlers: createBunWsHandlers,
29
35
  attach: attachWebSocket,
30
- })
36
+ }
37
+
38
+ /** The install seam a server exposes so the `websocket()` plugin can hand it the runtime. */
39
+ interface WsInstallable {
40
+ [INSTALL_WS](runtime: WsRuntime): void
41
+ }
42
+
43
+ /**
44
+ * Enable WebSocket routes on a server: `.use(websocket())` turns on `app.ws()`. Applying it twice is
45
+ * a no-op (named plugin dedupe).
46
+ */
47
+ export function websocket(): IdentityPlugin {
48
+ const apply = <S extends AnyServer>(app: S): S => {
49
+ ;(app as unknown as WsInstallable)[INSTALL_WS](WS_RUNTIME)
50
+ return app
51
+ }
52
+ return Object.assign(apply, { pluginName: "nifra:websocket" }) as IdentityPlugin
53
+ }
31
54
 
32
55
  export type {
33
56
  NifraWebSocket,
@@ -1,70 +0,0 @@
1
- /**
2
- * Contract-generated invariant tests: the declared contract *is* the test suite.
3
- *
4
- * `runContractInvariants(app)` reads each route's reflected contract (schemas, classification) and
5
- * verifies, with deterministic seeded fuzzing, the properties the contract already promises:
6
- *
7
- * 1. **Classification consistency** (static) — a route whose response fields are tagged more
8
- * sensitive than its declared route-level classification is understating what it emits.
9
- * 2. **Valid input never crashes** (dynamic) — schema-generated valid requests must not produce 5xx.
10
- * 3. **Response conformance** (dynamic) — a 2xx response body must validate against the declared
11
- * response schema; an undeclared response shape is exactly where sensitive data leaks.
12
- * 4. **Validation fails closed** (dynamic) — schema-violating bodies must be rejected (4xx), never
13
- * accepted (2xx) and never crash (5xx).
14
- *
15
- * Everything is deterministic: a seeded PRNG (no ambient randomness, no wall clock), so a failing
16
- * case reproduces from its seed. Routes whose inputs cannot be generated from their JSON Schema are
17
- * reported in `skipped` — never silently dropped.
18
- */
19
- export type InvariantFindingCode = "classification-understated" | "server-error-on-valid-input" | "response-schema-violation" | "validation-bypass" | "server-error-on-invalid-input";
20
- export interface InvariantFinding {
21
- readonly code: InvariantFindingCode;
22
- readonly method: string;
23
- readonly path: string;
24
- /** The case's deterministic seed — rerun with it to reproduce the exact request. */
25
- readonly seed?: number;
26
- readonly message: string;
27
- }
28
- export interface SkippedRoute {
29
- readonly method: string;
30
- readonly path: string;
31
- readonly reason: string;
32
- }
33
- export interface InvariantReport {
34
- readonly ok: boolean;
35
- /** Routes exercised dynamically. */
36
- readonly tested: readonly {
37
- readonly method: string;
38
- readonly path: string;
39
- }[];
40
- /** Routes that could not be exercised, with the reason — reported, never silently dropped. */
41
- readonly skipped: readonly SkippedRoute[];
42
- readonly findings: readonly InvariantFinding[];
43
- }
44
- export interface RunInvariantsOptions {
45
- /** Deterministic seed for the fuzzer. Default 1. */
46
- readonly seed?: number;
47
- /** Valid-input cases per route. Default 8. */
48
- readonly casesPerRoute?: number;
49
- /** Invalid-input (mutated) cases per route with a body schema. Default 4. */
50
- readonly invalidCasesPerRoute?: number;
51
- /**
52
- * Explicit isolated executor for dynamic cases. It must point at a disposable test app, sandbox,
53
- * or transaction that is rolled back. The runner never calls `source.fetch()` implicitly: doing so
54
- * could charge cards, send mail, or mutate production state merely by running a verification tool.
55
- */
56
- readonly executor?: InvariantExecutor;
57
- }
58
- export type InvariantExecutor = (request: Request) => Response | Promise<Response>;
59
- /** Opaque route-reflection source. The runner never invokes an app's live fetch method implicitly. */
60
- type ReflectableApp = object;
61
- /** Deterministic PRNG (mulberry32). Ambient randomness would make failures unreproducible. */
62
- export declare function createSeededRandom(seed: number): () => number;
63
- /** Run the contract-derived invariant suite against a reflectable app. Pure; no ambient state. */
64
- /**
65
- * @deprecated Use `runAdversarialContract` from `@nifrajs/testing`. The testing package is the
66
- * authoritative deep contract laboratory and powers CLI L4; this export remains for compatibility.
67
- */
68
- export declare function runContractInvariants(app: ReflectableApp, options?: RunInvariantsOptions): Promise<InvariantReport>;
69
- export {};
70
- //# sourceMappingURL=invariants.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"invariants.d.ts","sourceRoot":"","sources":["../src/invariants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAWH,MAAM,MAAM,oBAAoB,GAC5B,4BAA4B,GAC5B,6BAA6B,GAC7B,2BAA2B,GAC3B,mBAAmB,GACnB,+BAA+B,CAAA;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAA;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,oFAAoF;IACpF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;IACpB,oCAAoC;IACpC,QAAQ,CAAC,MAAM,EAAE,SAAS;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC9E,8FAA8F;IAC9F,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IACzC,QAAQ,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAA;CAC/C;AAED,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,8CAA8C;IAC9C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAA;IACtC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAA;CACtC;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAElF,sGAAsG;AACtG,KAAK,cAAc,GAAG,MAAM,CAAA;AA0B5B,8FAA8F;AAC9F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,MAAM,CAS7D;AA8ND,kGAAkG;AAClG;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,cAAc,EACnB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,eAAe,CAAC,CAmG1B"}