api-ape 3.0.1 → 4.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 (186) hide show
  1. package/README.md +58 -570
  2. package/client/README.md +73 -14
  3. package/client/auth/crypto/aead.js +214 -0
  4. package/client/auth/crypto/constants.js +32 -0
  5. package/client/auth/crypto/encoding.js +104 -0
  6. package/client/auth/crypto/files.md +27 -0
  7. package/client/auth/crypto/kdf.js +217 -0
  8. package/client/auth/crypto-utils.js +118 -0
  9. package/client/auth/files.md +52 -0
  10. package/client/auth/key-recovery.js +288 -0
  11. package/client/auth/recovery/constants.js +37 -0
  12. package/client/auth/recovery/files.md +23 -0
  13. package/client/auth/recovery/key-derivation.js +61 -0
  14. package/client/auth/recovery/sss-browser.js +189 -0
  15. package/client/auth/share-storage.js +205 -0
  16. package/client/auth/storage/constants.js +18 -0
  17. package/client/auth/storage/db.js +132 -0
  18. package/client/auth/storage/files.md +27 -0
  19. package/client/auth/storage/keys.js +173 -0
  20. package/client/auth/storage/shares.js +200 -0
  21. package/client/browser.js +190 -23
  22. package/client/connectSocket.js +418 -988
  23. package/client/connection/README.md +23 -0
  24. package/client/connection/fileDownload.js +256 -0
  25. package/client/connection/fileHandling.js +450 -0
  26. package/client/connection/fileUtils.js +346 -0
  27. package/client/connection/files.md +71 -0
  28. package/client/connection/messageHandler.js +105 -0
  29. package/client/connection/network.js +350 -0
  30. package/client/connection/proxy.js +233 -0
  31. package/client/connection/sender.js +333 -0
  32. package/client/connection/state.js +321 -0
  33. package/client/connection/subscriptions.js +151 -0
  34. package/client/files.md +53 -0
  35. package/client/index.js +298 -142
  36. package/client/transports/README.md +50 -0
  37. package/client/transports/files.md +41 -0
  38. package/client/transports/streamParser.js +195 -0
  39. package/client/transports/streaming.js +555 -202
  40. package/dist/ape.js +6 -1
  41. package/dist/ape.js.map +4 -4
  42. package/index.d.ts +38 -16
  43. package/package.json +32 -7
  44. package/server/README.md +287 -53
  45. package/server/adapters/README.md +28 -19
  46. package/server/adapters/files.md +68 -0
  47. package/server/adapters/firebase.js +543 -160
  48. package/server/adapters/index.js +362 -112
  49. package/server/adapters/mongo.js +530 -140
  50. package/server/adapters/postgres.js +534 -155
  51. package/server/adapters/redis.js +508 -143
  52. package/server/adapters/supabase.js +555 -186
  53. package/server/client/README.md +43 -0
  54. package/server/client/connection.js +586 -0
  55. package/server/client/files.md +40 -0
  56. package/server/client/index.js +342 -0
  57. package/server/files.md +54 -0
  58. package/server/index.js +332 -27
  59. package/server/lib/README.md +26 -0
  60. package/server/lib/broadcast/clients.js +219 -0
  61. package/server/lib/broadcast/files.md +58 -0
  62. package/server/lib/broadcast/index.js +57 -0
  63. package/server/lib/broadcast/publishProxy.js +110 -0
  64. package/server/lib/broadcast/pubsub.js +137 -0
  65. package/server/lib/broadcast/sendProxy.js +103 -0
  66. package/server/lib/bun.js +315 -99
  67. package/server/lib/fileTransfer/README.md +63 -0
  68. package/server/lib/fileTransfer/files.md +30 -0
  69. package/server/lib/fileTransfer/streaming.js +435 -0
  70. package/server/lib/fileTransfer.js +710 -326
  71. package/server/lib/files.md +111 -0
  72. package/server/lib/httpUtils.js +283 -0
  73. package/server/lib/loader.js +208 -7
  74. package/server/lib/longPolling/README.md +63 -0
  75. package/server/lib/longPolling/files.md +44 -0
  76. package/server/lib/longPolling/getHandler.js +365 -0
  77. package/server/lib/longPolling/postHandler.js +327 -0
  78. package/server/lib/longPolling.js +174 -221
  79. package/server/lib/main.js +369 -532
  80. package/server/lib/runtimes/README.md +42 -0
  81. package/server/lib/runtimes/bun.js +586 -0
  82. package/server/lib/runtimes/files.md +56 -0
  83. package/server/lib/runtimes/node.js +511 -0
  84. package/server/lib/wiring.js +539 -98
  85. package/server/lib/ws/README.md +35 -0
  86. package/server/lib/ws/adapters/README.md +54 -0
  87. package/server/lib/ws/adapters/bun.js +538 -170
  88. package/server/lib/ws/adapters/deno.js +623 -149
  89. package/server/lib/ws/adapters/files.md +42 -0
  90. package/server/lib/ws/files.md +74 -0
  91. package/server/lib/ws/frames.js +532 -154
  92. package/server/lib/ws/index.js +207 -10
  93. package/server/lib/ws/server.js +385 -92
  94. package/server/lib/ws/socket.js +549 -181
  95. package/server/lib/wsProvider.js +363 -89
  96. package/server/plugins/binary.js +282 -0
  97. package/server/security/README.md +92 -0
  98. package/server/security/auth/README.md +319 -0
  99. package/server/security/auth/adapters/files.md +95 -0
  100. package/server/security/auth/adapters/ldap/constants.js +37 -0
  101. package/server/security/auth/adapters/ldap/files.md +19 -0
  102. package/server/security/auth/adapters/ldap/helpers.js +111 -0
  103. package/server/security/auth/adapters/ldap.js +353 -0
  104. package/server/security/auth/adapters/oauth2/constants.js +41 -0
  105. package/server/security/auth/adapters/oauth2/files.md +19 -0
  106. package/server/security/auth/adapters/oauth2/helpers.js +123 -0
  107. package/server/security/auth/adapters/oauth2.js +273 -0
  108. package/server/security/auth/adapters/opaque-handlers.js +314 -0
  109. package/server/security/auth/adapters/opaque.js +205 -0
  110. package/server/security/auth/adapters/saml/constants.js +52 -0
  111. package/server/security/auth/adapters/saml/files.md +19 -0
  112. package/server/security/auth/adapters/saml/helpers.js +74 -0
  113. package/server/security/auth/adapters/saml.js +173 -0
  114. package/server/security/auth/adapters/totp.js +703 -0
  115. package/server/security/auth/adapters/webauthn.js +625 -0
  116. package/server/security/auth/files.md +61 -0
  117. package/server/security/auth/framework/constants.js +27 -0
  118. package/server/security/auth/framework/files.md +23 -0
  119. package/server/security/auth/framework/handlers.js +272 -0
  120. package/server/security/auth/framework/socket-auth.js +177 -0
  121. package/server/security/auth/handlers/auth-messages.js +143 -0
  122. package/server/security/auth/handlers/files.md +28 -0
  123. package/server/security/auth/index.js +290 -0
  124. package/server/security/auth/mfa/crypto/aead.js +148 -0
  125. package/server/security/auth/mfa/crypto/constants.js +35 -0
  126. package/server/security/auth/mfa/crypto/files.md +27 -0
  127. package/server/security/auth/mfa/crypto/kdf.js +120 -0
  128. package/server/security/auth/mfa/crypto/utils.js +68 -0
  129. package/server/security/auth/mfa/crypto-utils.js +80 -0
  130. package/server/security/auth/mfa/files.md +77 -0
  131. package/server/security/auth/mfa/ledger/constants.js +75 -0
  132. package/server/security/auth/mfa/ledger/errors.js +73 -0
  133. package/server/security/auth/mfa/ledger/files.md +23 -0
  134. package/server/security/auth/mfa/ledger/share-record.js +32 -0
  135. package/server/security/auth/mfa/ledger.js +255 -0
  136. package/server/security/auth/mfa/recovery/constants.js +67 -0
  137. package/server/security/auth/mfa/recovery/files.md +19 -0
  138. package/server/security/auth/mfa/recovery/handlers.js +216 -0
  139. package/server/security/auth/mfa/recovery.js +191 -0
  140. package/server/security/auth/mfa/sss/constants.js +21 -0
  141. package/server/security/auth/mfa/sss/files.md +23 -0
  142. package/server/security/auth/mfa/sss/gf256.js +103 -0
  143. package/server/security/auth/mfa/sss/serialization.js +82 -0
  144. package/server/security/auth/mfa/sss.js +161 -0
  145. package/server/security/auth/mfa/two-of-three/constants.js +58 -0
  146. package/server/security/auth/mfa/two-of-three/files.md +23 -0
  147. package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
  148. package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
  149. package/server/security/auth/mfa/two-of-three.js +136 -0
  150. package/server/security/auth/nonce-manager.js +89 -0
  151. package/server/security/auth/state-machine-mfa.js +269 -0
  152. package/server/security/auth/state-machine.js +257 -0
  153. package/server/security/extractRootDomain.js +144 -16
  154. package/server/security/files.md +51 -0
  155. package/server/security/origin.js +197 -15
  156. package/server/security/reply.js +274 -16
  157. package/server/socket/README.md +119 -0
  158. package/server/socket/authMiddleware.js +299 -0
  159. package/server/socket/files.md +86 -0
  160. package/server/socket/open.js +154 -8
  161. package/server/socket/pluginHooks.js +334 -0
  162. package/server/socket/receive.js +184 -225
  163. package/server/socket/receiveContext.js +117 -0
  164. package/server/socket/send.js +416 -78
  165. package/server/socket/tagUtils.js +402 -0
  166. package/server/utils/README.md +19 -0
  167. package/server/utils/deepRequire.js +255 -30
  168. package/server/utils/files.md +57 -0
  169. package/server/utils/genId.js +182 -20
  170. package/server/utils/parseUserAgent.js +313 -251
  171. package/server/utils/userAgent/README.md +65 -0
  172. package/server/utils/userAgent/files.md +46 -0
  173. package/server/utils/userAgent/patterns.js +545 -0
  174. package/utils/README.md +21 -0
  175. package/utils/files.md +66 -0
  176. package/utils/jss/README.md +21 -0
  177. package/utils/jss/decode.js +471 -0
  178. package/utils/jss/encode.js +312 -0
  179. package/utils/jss/files.md +68 -0
  180. package/utils/jss/plugins.js +210 -0
  181. package/utils/jss.js +219 -273
  182. package/utils/messageHash.js +238 -35
  183. package/dist/api-ape.min.js +0 -2
  184. package/dist/api-ape.min.js.map +0 -7
  185. package/server/client.js +0 -308
  186. package/server/lib/broadcast.js +0 -146
@@ -0,0 +1,312 @@
1
+ /**
2
+ * @fileoverview JSS Encoder - Encodes JavaScript objects to JSS format
3
+ *
4
+ * This module provides the encoding functionality for JSON Super Set (JSS).
5
+ * It converts JavaScript objects containing extended types (Date, RegExp,
6
+ * Error, Map, Set, undefined) into a JSON-compatible format using tagged keys.
7
+ *
8
+ * ## Encoding Process
9
+ *
10
+ * The encoder traverses the object tree and:
11
+ * 1. Detects extended types using `Object.prototype.toString`
12
+ * 2. Converts them to primitive representations
13
+ * 3. Tags the key with a type indicator (e.g., `<!D>` for Date)
14
+ * 4. Tracks visited objects to handle circular references
15
+ *
16
+ * ## Tag System
17
+ *
18
+ * | Type | Tag | Encoded Value |
19
+ * |-----------|-----|----------------------------------------|
20
+ * | Date | `D` | Unix timestamp (milliseconds) |
21
+ * | RegExp | `R` | String representation (e.g., "/a/gi") |
22
+ * | Error | `E` | Array: [name, message, stack] |
23
+ * | undefined | `U` | null |
24
+ * | Map | `M` | Object from entries |
25
+ * | Set | `S` | Array of values |
26
+ * | Pointer | `P` | Path array to referenced object |
27
+ *
28
+ * ## Array Type Tags
29
+ *
30
+ * For arrays containing extended types, the tag includes all element types:
31
+ * ```javascript
32
+ * [new Date(), new Date()] → { "[D,D]": [timestamp1, timestamp2] }
33
+ * ```
34
+ *
35
+ * @module utils/jss/encode
36
+ * @see {@link module:utils/jss/decode} for decoding implementation
37
+ * @see {@link module:utils/jss} for main JSS module
38
+ *
39
+ * @example
40
+ * const { encode, stringify } = require('./encode')
41
+ *
42
+ * // Encode without stringifying (returns plain object)
43
+ * const encoded = encode({
44
+ * created: new Date('2024-01-01'),
45
+ * pattern: /test/i
46
+ * })
47
+ * // { "created<!D>": 1704067200000, "pattern<!R>": "/test/i" }
48
+ *
49
+ * // Stringify (encode + JSON.stringify)
50
+ * const str = stringify({ date: new Date() })
51
+ * // Ready for transmission
52
+ *
53
+ * @example
54
+ * // Circular reference handling
55
+ * const obj = { name: 'root' }
56
+ * obj.self = obj
57
+ *
58
+ * const encoded = encode(obj)
59
+ * // { name: 'root', 'self<!P>': [] }
60
+ * // The empty array [] is the path to the root object
61
+ */
62
+
63
+ /**
64
+ * Lookup table mapping Object.prototype.toString results to type tags
65
+ *
66
+ * Used for fast type detection during encoding. Only types that require
67
+ * special handling are included - standard JSON types (string, number,
68
+ * boolean, null, array, object) are handled separately.
69
+ *
70
+ * @constant {Object.<string, string>}
71
+ * @private
72
+ *
73
+ * @example
74
+ * Object.prototype.toString.call(new Date()) // '[object Date]'
75
+ * tagLookup['[object Date]'] // 'D'
76
+ */
77
+ const { getAllPlugins } = require("./plugins");
78
+
79
+ const tagLookup = {
80
+ /**
81
+ * RegExp objects - serialized as string pattern
82
+ */
83
+ "[object RegExp]": "R",
84
+
85
+ /**
86
+ * Date objects - serialized as Unix timestamp
87
+ */
88
+ "[object Date]": "D",
89
+
90
+ /**
91
+ * Error objects - serialized as [name, message, stack] array
92
+ */
93
+ "[object Error]": "E",
94
+
95
+ /**
96
+ * Undefined values - explicitly encoded (unlike JSON which omits them)
97
+ */
98
+ "[object Undefined]": "U",
99
+
100
+ /**
101
+ * Map objects - serialized as plain object from entries
102
+ */
103
+ "[object Map]": "M",
104
+
105
+ /**
106
+ * Set objects - serialized as array of values
107
+ */
108
+ "[object Set]": "S",
109
+ };
110
+
111
+ /**
112
+ * Encode a JavaScript object to JSS format
113
+ *
114
+ * Recursively traverses the object tree, converting extended types to
115
+ * their tagged representations. Handles circular references by tracking
116
+ * visited objects and replacing subsequent references with path pointers.
117
+ *
118
+ * ## Algorithm
119
+ *
120
+ * 1. Create a WeakMap to track visited objects and their paths
121
+ * 2. For each value in the object:
122
+ * - If it's an extended type (Date, RegExp, etc.), encode it
123
+ * - If it's an object/array, recurse (checking for circularity)
124
+ * - If it's a primitive, pass through unchanged
125
+ * 3. Return the encoded object structure
126
+ *
127
+ * ## Circular Reference Detection
128
+ *
129
+ * When an object is first encountered, its path is stored in `visitedEncode`.
130
+ * If the same object is encountered again, a pointer (`P` tag) is created
131
+ * with the stored path.
132
+ *
133
+ * @param {any} obj - The object to encode
134
+ * @returns {Object} Encoded object with tagged keys for extended types
135
+ *
136
+ * @example
137
+ * // Simple types
138
+ * encode({ date: new Date('2024-01-01') })
139
+ * // { "date<!D>": 1704067200000 }
140
+ *
141
+ * @example
142
+ * // Nested structures
143
+ * encode({
144
+ * user: {
145
+ * name: 'Alice',
146
+ * createdAt: new Date(),
147
+ * roles: new Set(['admin', 'user'])
148
+ * }
149
+ * })
150
+ * // {
151
+ * // user: {
152
+ * // name: 'Alice',
153
+ * // "createdAt<!D>": 1704067200000,
154
+ * // "roles<!S>": ['admin', 'user']
155
+ * // }
156
+ * // }
157
+ *
158
+ * @example
159
+ * // Circular references
160
+ * const a = { name: 'a' }
161
+ * const b = { name: 'b', ref: a }
162
+ * a.ref = b
163
+ *
164
+ * encode({ a, b })
165
+ * // {
166
+ * // a: { name: 'a', 'ref<!P>': ['b'] },
167
+ * // b: { name: 'b', 'ref<!P>': ['a'] }
168
+ * // }
169
+ *
170
+ * @example
171
+ * // Error objects
172
+ * encode({ error: new TypeError('Invalid input') })
173
+ * // { "error<!E>": ['TypeError', 'Invalid input', 'TypeError: Invalid input\n at ...'] }
174
+ *
175
+ * @example
176
+ * // Mixed array with extended types
177
+ * encode({ dates: [new Date(), new Date()] })
178
+ * // { "dates<![D,D]>": [1704067200000, 1704067300000] }
179
+ */
180
+ function encode(obj) {
181
+ /**
182
+ * WeakMap tracking visited objects to detect circular references
183
+ * Maps each visited object to its path in the object tree
184
+ * @type {WeakMap<Object, Array<string|number>>}
185
+ */
186
+ const visitedEncode = new WeakMap();
187
+ visitedEncode.set(obj, []);
188
+
189
+ /**
190
+ * Recursively encode a value with circular reference tracking
191
+ *
192
+ * @param {any} value - The value to encode
193
+ * @param {Array<string|number>} path - Current path in the object tree
194
+ * @returns {[string, any]} Tuple of [tag, encodedValue]
195
+ * @private
196
+ */
197
+ function encodeValueWithVisited(value, path = []) {
198
+ const type = typeof value;
199
+ const tag = tagLookup[Object.prototype.toString.call(value)];
200
+
201
+ // Handle special types with known tags
202
+ if (tag !== undefined) {
203
+ if ("D" === tag) return [tag, value.valueOf()];
204
+ if ("E" === tag) return [tag, [value.name, value.message, value.stack]];
205
+ if ("R" === tag) return [tag, value.toString()];
206
+ if ("U" === tag) return [tag, null];
207
+ if ("S" === tag) return [tag, Array.from(value)];
208
+ if ("M" === tag) return [tag, Object.fromEntries(value)];
209
+ }
210
+
211
+ // Check custom plugins
212
+ for (const [customTag, plugin] of getAllPlugins()) {
213
+ const key = path.length > 0 ? path[path.length - 1] : undefined;
214
+ if (plugin.check(key, value)) {
215
+ return [customTag, plugin.encode(path, key, value, {})];
216
+ }
217
+ }
218
+
219
+ // Handle objects and arrays (potential circular references)
220
+ if (type === "object" && value !== null) {
221
+ // Check for circular reference
222
+ if (visitedEncode.has(value)) return ["P", visitedEncode.get(value)];
223
+
224
+ // Mark as visited with current path
225
+ visitedEncode.set(value, path);
226
+
227
+ const isArray = Array.isArray(value);
228
+ const objKeys = isArray
229
+ ? Array.from(Array(value.length).keys())
230
+ : Object.keys(value);
231
+ const result = isArray ? [] : {};
232
+ const typesFound = [];
233
+
234
+ // Process each property/element
235
+ for (let i = 0; i < objKeys.length; i++) {
236
+ const key = objKeys[i];
237
+ const [t, v] = encodeValueWithVisited(value[key], [...path, key]);
238
+
239
+ if (isArray) {
240
+ typesFound.push(t);
241
+ result.push(v);
242
+ } else if (value[key] !== undefined) {
243
+ // Add tag to key if value was special type
244
+ result[key + (t ? `<!${t}>` : "")] = v;
245
+ }
246
+ }
247
+
248
+ // For arrays with special types, create compound tag
249
+ if (isArray && typesFound.find((t) => !!t))
250
+ return [`[${typesFound.join()}]`, result];
251
+ return ["", result];
252
+ }
253
+ // Primitive values pass through unchanged
254
+ else {
255
+ return ["", value];
256
+ }
257
+ }
258
+
259
+ // Process root object properties
260
+ let keys = Array.isArray(obj)
261
+ ? Array.from(Array(obj.length).keys())
262
+ : Object.keys(obj);
263
+ const result = {};
264
+
265
+ for (let i = 0; i < keys.length; i++) {
266
+ const key = keys[i];
267
+ if (obj[key] !== undefined) {
268
+ const [t, v] = encodeValueWithVisited(obj[key], [key]);
269
+ result[key + (t ? `<!${t}>` : "")] = v;
270
+ }
271
+ }
272
+
273
+ return result;
274
+ }
275
+
276
+ /**
277
+ * Stringify an object to JSS format
278
+ *
279
+ * Combines `encode()` with `JSON.stringify()` to produce a string
280
+ * representation suitable for transmission over WebSocket or storage.
281
+ *
282
+ * This is the high-level API - use this for most cases.
283
+ *
284
+ * @param {any} obj - The object to stringify
285
+ * @returns {string} JSS-encoded JSON string
286
+ *
287
+ * @example
288
+ * // Basic usage
289
+ * const str = stringify({
290
+ * message: 'Hello',
291
+ * timestamp: new Date(),
292
+ * pattern: /world/i
293
+ * })
294
+ * // '{"message":"Hello","timestamp<!D>":1704067200000,"pattern<!R>":"/world/i"}'
295
+ *
296
+ * @example
297
+ * // With nested objects
298
+ * const str = stringify({
299
+ * user: {
300
+ * settings: new Map([['theme', 'dark']])
301
+ * }
302
+ * })
303
+ *
304
+ * @example
305
+ * // Ready for WebSocket transmission
306
+ * socket.send(stringify({ type: '/chat', data: { text: 'Hi!' } }))
307
+ */
308
+ function stringify(obj) {
309
+ return JSON.stringify(encode(obj));
310
+ }
311
+
312
+ module.exports = { encode, stringify };
@@ -0,0 +1,68 @@
1
+ # JSS Sub-Modules Files
2
+
3
+ This module contains the low-level encoding and decoding implementations for JSON Super Set (JSS). The encoder traverses objects to detect and tag extended types, while the decoder parses tagged keys and reconstructs the original JavaScript types.
4
+
5
+ ## Guidelines
6
+
7
+ - **Tag consistency** — Encoding and decoding tags must match exactly; if you add a new tag to `encode.js`, add the corresponding decoder to `decode.js`
8
+ - **Type detection** — Use `Object.prototype.toString.call()` for reliable type detection, not `instanceof`
9
+ - **Circular reference tracking** — Both encoder and decoder must handle circular references via the `<!P>` path pointer tag
10
+ - **Isomorphic code** — All code must work in both browser and Node.js/Bun/Deno environments
11
+ - **Preserve error stacks** — Error encoding must preserve the original stack trace for debugging
12
+ - **No external dependencies** — Pure JavaScript only; no external packages
13
+
14
+ ## Directory Structure
15
+
16
+ ```
17
+ jss/
18
+ ├── encode.js # JSS encoding (object → tagged JSON-compatible format)
19
+ └── decode.js # JSS decoding (tagged format → restored JavaScript types)
20
+ ```
21
+
22
+ ## Files
23
+
24
+ ### `encode.js`
25
+
26
+ Converts JavaScript objects containing extended types into JSON-compatible format:
27
+
28
+ - Detects types via `Object.prototype.toString`
29
+ - Tags keys with type indicators (`<!D>`, `<!R>`, `<!E>`, `<!U>`, `<!M>`, `<!S>`, `<!P>`)
30
+ - Converts Dates to timestamps, RegExps to strings, Errors to arrays
31
+ - Tracks visited objects to handle circular references
32
+
33
+ **Exports:**
34
+
35
+ - `encode(obj)` — Returns JSS-encoded plain object (for inspection or custom serialization)
36
+ - `stringify(obj)` — Returns JSS-encoded JSON string (encode + JSON.stringify)
37
+
38
+ **Tag Reference:**
39
+
40
+ | Tag | Type | Encoded Value |
41
+ |-----|------|---------------|
42
+ | `<!D>` | Date | Unix timestamp (milliseconds) |
43
+ | `<!R>` | RegExp | String representation (e.g., "/test/gi") |
44
+ | `<!E>` | Error | Array: [name, message, stack] |
45
+ | `<!U>` | undefined | null |
46
+ | `<!M>` | Map | Object from entries |
47
+ | `<!S>` | Set | Array of values |
48
+ | `<!P>` | Pointer | Path array to referenced object (circular refs) |
49
+
50
+ ### `decode.js`
51
+
52
+ Restores JavaScript types from JSS-encoded format:
53
+
54
+ - Parses tagged keys to identify encoded types
55
+ - Reconstructs Date, RegExp, Error, Map, Set, and undefined
56
+ - Resolves circular reference pointers to restore object cycles
57
+ - Handles nested structures recursively
58
+
59
+ **Exports:**
60
+
61
+ - `decode(obj)` — Decodes JSS-encoded plain object back to original types
62
+ - `parse(str)` — Parses JSS string and decodes (JSON.parse + decode)
63
+
64
+ **Decoding Process:**
65
+
66
+ 1. First pass: Decode all values, storing pointer locations
67
+ 2. Second pass: Resolve `<!P>` pointers by following stored paths
68
+ 3. Return fully restored object with original types and circular references
@@ -0,0 +1,210 @@
1
+ /**
2
+ * @fileoverview JSS Plugin Registry
3
+ *
4
+ * This module provides the plugin registration system for JSS (JSON Super Set).
5
+ * It allows developers to register custom type handlers that extend JSS beyond
6
+ * its built-in types (Date, RegExp, Error, etc.).
7
+ *
8
+ * ## Plugin Architecture
9
+ *
10
+ * Plugins are registered with a single-character tag and provide:
11
+ * - `check(key, value)` - Determines if this plugin handles a value
12
+ * - `encode(path, key, value, context)` - Transforms value for serialization
13
+ * - `decode(value, path, context)` - Restores value from serialization
14
+ * - `onSend(path, key, value, context)` - Optional: handles external resources during send
15
+ * - `onReceive(path, key, value, context)` - Optional: handles external resources during receive
16
+ *
17
+ * ## Usage
18
+ *
19
+ * ```javascript
20
+ * const jss = require('./jss')
21
+ *
22
+ * jss.custom('X', {
23
+ * check: (key, value) => value instanceof CustomType,
24
+ * encode: (path, key, value, ctx) => value.toSerializable(),
25
+ * decode: (value, path, ctx) => CustomType.fromSerializable(value)
26
+ * })
27
+ * ```
28
+ *
29
+ * @module utils/jss/plugins
30
+ * @see {@link module:utils/jss} for main JSS module
31
+ */
32
+
33
+ /**
34
+ * Built-in type tags that cannot be overridden
35
+ * @constant {string[]}
36
+ */
37
+ const builtInTags = ["D", "R", "E", "U", "M", "S", "P", "I"];
38
+
39
+ /**
40
+ * Registry of custom plugins
41
+ * Maps tag character to plugin configuration
42
+ * @type {Map<string, PluginConfig>}
43
+ * @private
44
+ */
45
+ const plugins = new Map();
46
+
47
+ /**
48
+ * @typedef {Object} PluginConfig
49
+ * @property {function(string|number, any): boolean} check - Determines if plugin handles value
50
+ * @property {function(string[], string|number, any, Object): any} encode - Transforms for serialization
51
+ * @property {function(any, string[], Object): any} decode - Restores from serialization
52
+ * @property {function(string[], string|number, any, Object): {replace: any, cleanup?: function}=} onSend - Optional send hook
53
+ * @property {function(string[], string|number, any, Object): Promise<any>=} onReceive - Optional receive hook
54
+ */
55
+
56
+ /**
57
+ * Register a custom type handler plugin
58
+ *
59
+ * Plugins extend JSS to handle custom types beyond the built-in set.
60
+ * Each plugin is identified by a single-character tag that appears in
61
+ * the serialized format (e.g., `"key<!X>": value`).
62
+ *
63
+ * ## Behavior Rules
64
+ *
65
+ * - **Check gates encode**: The `check` function determines if this plugin
66
+ * should handle a value. If it returns true, encode is called.
67
+ * - **Error on conflict**: Throws if the tag conflicts with a built-in type
68
+ * or an already-registered custom plugin.
69
+ *
70
+ * @param {string} tag - Single character tag identifier (e.g., 'X', 'Z')
71
+ * @param {PluginConfig} config - Plugin configuration object
72
+ * @throws {Error} If tag is not a single character
73
+ * @throws {Error} If tag conflicts with built-in type
74
+ * @throws {Error} If tag is already registered
75
+ * @throws {Error} If required functions are missing
76
+ *
77
+ * @example
78
+ * // Register a plugin for a custom Point type
79
+ * register('P', {
80
+ * check: (key, value) => value instanceof Point,
81
+ * encode: (path, key, value) => [value.x, value.y],
82
+ * decode: (value) => new Point(value[0], value[1])
83
+ * })
84
+ *
85
+ * @example
86
+ * // Register a plugin with lifecycle hooks for external resources
87
+ * register('L', {
88
+ * check: (key, value) => Buffer.isBuffer(value),
89
+ * encode: (path, key, value) => '__pending__',
90
+ * decode: (value) => value, // Hash returned as-is
91
+ * onSend: (path, key, value, ctx) => {
92
+ * const hash = generateHash(ctx.queryId, path.join('.'))
93
+ * ctx.fileTransfer.registerDownload(hash, value, 'application/octet-stream', ctx.clientId)
94
+ * return { replace: hash }
95
+ * }
96
+ * })
97
+ */
98
+ function register(tag, config) {
99
+ // Validate tag format
100
+ if (typeof tag !== "string" || tag.length !== 1) {
101
+ throw new Error(`Tag must be a single character, got: '${tag}'`);
102
+ }
103
+
104
+ // Check for built-in tag conflict
105
+ if (builtInTags.includes(tag)) {
106
+ throw new Error(`Tag '${tag}' conflicts with built-in type`);
107
+ }
108
+
109
+ // Check for duplicate registration
110
+ if (plugins.has(tag)) {
111
+ throw new Error(`Tag '${tag}' is already registered`);
112
+ }
113
+
114
+ // Validate required functions
115
+ if (typeof config.check !== "function") {
116
+ throw new Error("Plugin must provide a 'check' function");
117
+ }
118
+ if (typeof config.encode !== "function") {
119
+ throw new Error("Plugin must provide an 'encode' function");
120
+ }
121
+ if (typeof config.decode !== "function") {
122
+ throw new Error("Plugin must provide a 'decode' function");
123
+ }
124
+
125
+ // Validate optional functions if provided
126
+ if (config.onSend !== undefined && typeof config.onSend !== "function") {
127
+ throw new Error("Plugin 'onSend' must be a function if provided");
128
+ }
129
+ if (
130
+ config.onReceive !== undefined &&
131
+ typeof config.onReceive !== "function"
132
+ ) {
133
+ throw new Error("Plugin 'onReceive' must be a function if provided");
134
+ }
135
+
136
+ plugins.set(tag, config);
137
+ }
138
+
139
+ /**
140
+ * Get a plugin by its tag
141
+ *
142
+ * @param {string} tag - The tag character to look up
143
+ * @returns {PluginConfig|undefined} The plugin config or undefined if not found
144
+ *
145
+ * @example
146
+ * const plugin = getPlugin('X')
147
+ * if (plugin) {
148
+ * const decoded = plugin.decode(value, path, context)
149
+ * }
150
+ */
151
+ function getPlugin(tag) {
152
+ return plugins.get(tag);
153
+ }
154
+
155
+ /**
156
+ * Get all registered plugins
157
+ *
158
+ * Returns the internal Map for iteration during encoding.
159
+ *
160
+ * @returns {Map<string, PluginConfig>} Map of tag -> plugin config
161
+ *
162
+ * @example
163
+ * for (const [tag, plugin] of getAllPlugins()) {
164
+ * if (plugin.check(key, value)) {
165
+ * return [tag, plugin.encode(path, key, value, context)]
166
+ * }
167
+ * }
168
+ */
169
+ function getAllPlugins() {
170
+ return plugins;
171
+ }
172
+
173
+ /**
174
+ * Check if a tag has a registered plugin
175
+ *
176
+ * @param {string} tag - The tag to check
177
+ * @returns {boolean} True if a plugin is registered for this tag
178
+ *
179
+ * @example
180
+ * if (hasPlugin('X')) {
181
+ * // Handle custom type
182
+ * }
183
+ */
184
+ function hasPlugin(tag) {
185
+ return plugins.has(tag);
186
+ }
187
+
188
+ /**
189
+ * Clear all registered plugins
190
+ *
191
+ * Used primarily for testing to reset the registry between tests.
192
+ * Does not affect built-in types.
193
+ *
194
+ * @example
195
+ * beforeEach(() => {
196
+ * clearPlugins()
197
+ * })
198
+ */
199
+ function clearPlugins() {
200
+ plugins.clear();
201
+ }
202
+
203
+ module.exports = {
204
+ register,
205
+ getPlugin,
206
+ getAllPlugins,
207
+ hasPlugin,
208
+ clearPlugins,
209
+ builtInTags,
210
+ };