api-ape 3.0.2 → 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 +59 -572
  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 -203
  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 +31 -6
  44. package/server/README.md +272 -67
  45. package/server/adapters/README.md +23 -14
  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 +322 -71
  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 -219
  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 -224
  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 -311
  186. package/server/lib/broadcast.js +0 -146
package/utils/files.md ADDED
@@ -0,0 +1,66 @@
1
+ # Utils Module Files
2
+
3
+ This module provides shared serialization and hashing utilities used by both client and server components of api-ape. These utilities enable the framework's ability to transparently handle complex JavaScript types over WebSocket connections.
4
+
5
+ ## Guidelines
6
+
7
+ - **Isomorphic code** — All utilities must work identically in browser and Node.js/Bun/Deno environments
8
+ - **No Node.js APIs** — Avoid `fs`, `path`, `Buffer` and other Node-specific APIs; use browser-compatible alternatives
9
+ - **JSS consistency** — Changes to encoding must be mirrored in decoding; tags must match exactly
10
+ - **Hash stability** — The `messageHash` algorithm must remain deterministic; changing it breaks request/response correlation
11
+ - **Circular reference support** — JSS must handle circular references via path pointers (`<!P>` tag)
12
+ - **Test coverage** — All utilities have corresponding `.test.js` files; update tests when modifying behavior
13
+
14
+ ## Directory Structure
15
+
16
+ ```
17
+ utils/
18
+ ├── jss.js # JSS main entry point (encode/decode/stringify/parse)
19
+ ├── jss.test.js # JSS test suite
20
+ ├── messageHash.js # Jenkins hash with Crockford Base32 encoding
21
+ ├── messageHash.test.js # Message hash test suite
22
+ ├── parseUserAgent.test.js # User-Agent parser test suite
23
+ └── jss/ # JSS encoder/decoder implementations
24
+ ```
25
+
26
+ ## Files
27
+
28
+ ### `jss.js`
29
+
30
+ Main entry point for JSON Super Set serialization. Re-exports `encode`, `decode`, `stringify`, and `parse` from the `jss/` subdirectory. This is a drop-in replacement for `JSON.stringify` and `JSON.parse` that handles extended types.
31
+
32
+ **Supported Types:**
33
+
34
+ | Type | Tag | Encoded As |
35
+ |------|-----|------------|
36
+ | Date | `<!D>` | Unix timestamp (milliseconds) |
37
+ | RegExp | `<!R>` | String pattern (e.g., "/test/gi") |
38
+ | Error | `<!E>` | Array: [name, message, stack] |
39
+ | undefined | `<!U>` | null |
40
+ | Map | `<!M>` | Object from entries |
41
+ | Set | `<!S>` | Array of values |
42
+ | Circular | `<!P>` | Path array to referenced object |
43
+
44
+ ### `messageHash.js`
45
+
46
+ Generates deterministic hash strings from message content using the Jenkins one-at-a-time hash algorithm with Crockford Base32 encoding. Used to correlate WebSocket requests with their responses via `queryId`.
47
+
48
+ - **Algorithm:** Jenkins one-at-a-time hash (32-bit)
49
+ - **Encoding:** Crockford Base32 (excludes I, L, O, U for readability)
50
+ - **Output:** 1-7 character URL-safe string
51
+
52
+ ### `jss.test.js`
53
+
54
+ Test suite for JSS serialization. Tests encoding/decoding of all supported types, circular references, and edge cases.
55
+
56
+ ### `messageHash.test.js`
57
+
58
+ Test suite for message hashing. Tests determinism, collision resistance, and Base32 encoding.
59
+
60
+ ### `parseUserAgent.test.js`
61
+
62
+ Test suite for the User-Agent parser (implementation in `server/utils/parseUserAgent.js`). Tests browser, OS, device, and bot detection against real User-Agent strings.
63
+
64
+ ### `jss/`
65
+
66
+ JSS encoder/decoder implementations. See [`jss/files.md`](./jss/files.md).
@@ -0,0 +1,21 @@
1
+ # JSS Sub-Modules
2
+
3
+ ## Overview
4
+
5
+ The jss sub-modules contain the low-level encoding and decoding implementations for JSON Super Set (JSS). These modules handle the actual conversion logic that transforms JavaScript extended types into JSON-compatible tagged representations and restores them back.
6
+
7
+ The encoder traverses objects to detect and tag extended types (Date, RegExp, Error, Map, Set, undefined), while the decoder parses tagged keys and reconstructs the original types. Both modules handle circular references through path-based pointers.
8
+
9
+ **Key capabilities:**
10
+
11
+ - **Type detection** — Identify JavaScript types that require special serialization
12
+ - **Tag encoding** — Convert extended types to primitives with tagged keys (e.g., `<!D>` for Date)
13
+ - **Tag decoding** — Restore tagged values back to their original JavaScript types
14
+ - **Circular reference handling** — Track and resolve object cycles via path pointers
15
+
16
+ > **Contributing?** See [`files.md`](./files.md) for directory structure and file descriptions.
17
+
18
+ ## See Also
19
+
20
+ - [`../jss.js`](../jss.js) — Main JSS module (re-exports encode/decode)
21
+ - [`../README.md`](../README.md) — Utils module overview
@@ -0,0 +1,471 @@
1
+ /**
2
+ * @fileoverview JSS Decoder - Decodes JSS Format Back to JavaScript Objects
3
+ *
4
+ * This module provides the decoding/parsing functionality for JSS (JSON Super Set).
5
+ * It reverses the encoding process, restoring JavaScript types from their tagged
6
+ * string representations.
7
+ *
8
+ * ## Decoding Process
9
+ *
10
+ * 1. Parse the JSON string (if using `parse()`)
11
+ * 2. Recursively traverse the object structure
12
+ * 3. Detect tagged keys (e.g., `key<!D>` for Date)
13
+ * 4. Apply the appropriate decoder for each tag
14
+ * 5. Resolve circular reference pointers
15
+ * 6. Return the fully restored object
16
+ *
17
+ * ## Supported Tags
18
+ *
19
+ * | Tag | Type | Decoder Behavior |
20
+ * |-----|-----------|-------------------------------------------|
21
+ * | `D` | Date | `new Date(timestamp)` |
22
+ * | `R` | RegExp | `new RegExp(pattern)` from string |
23
+ * | `E` | Error | Reconstructs with name, message, stack |
24
+ * | `U` | undefined | Returns `undefined` value |
25
+ * | `M` | Map | `new Map(Object.entries(obj))` |
26
+ * | `S` | Set | `new Set(array)` |
27
+ * | `P` | Pointer | Circular reference (resolved after parse) |
28
+ *
29
+ * ## Circular Reference Resolution
30
+ *
31
+ * Circular references are encoded as path pointers. During decoding:
32
+ * 1. First pass: Decode all values, storing pointer locations
33
+ * 2. Second pass: Resolve pointers by following stored paths
34
+ *
35
+ * ```javascript
36
+ * // Encoded: { "self<!P>": [] } // Pointer to root
37
+ * // Decoded: obj.self === obj // Circular reference restored
38
+ * ```
39
+ *
40
+ * @module utils/jss/decode
41
+ * @see {@link module:utils/jss/encode} for the encoding counterpart
42
+ * @see {@link module:utils/jss} for the main JSS module
43
+ *
44
+ * @example
45
+ * // Basic decoding
46
+ * const { parse } = require('./decode')
47
+ *
48
+ * const result = parse('{"timestamp<!D>":1704067200000}')
49
+ * console.log(result.timestamp instanceof Date) // true
50
+ * console.log(result.timestamp.toISOString()) // '2024-01-01T00:00:00.000Z'
51
+ *
52
+ * @example
53
+ * // Decoding errors with preserved stack traces
54
+ * const { parse } = require('./decode')
55
+ *
56
+ * const result = parse('{"err<!E>":["TypeError","Invalid input","Error: Invalid input\\n at ..."]}')
57
+ * console.log(result.err instanceof TypeError) // true
58
+ * console.log(result.err.message) // 'Invalid input'
59
+ * console.log(result.err.stack) // Original stack trace
60
+ *
61
+ * @example
62
+ * // Low-level decode without JSON parsing
63
+ * const { decode } = require('./decode')
64
+ *
65
+ * const encoded = { "items<!S>": [1, 2, 3], "config<!M>": { a: 1, b: 2 } }
66
+ * const decoded = decode(encoded)
67
+ * console.log(decoded.items instanceof Set) // true
68
+ * console.log(decoded.config instanceof Map) // true
69
+ */
70
+
71
+ /**
72
+ * Temporary storage for circular reference pointers during decoding
73
+ *
74
+ * Each entry is a tuple of [sourcePath, targetPath] where:
75
+ * - sourcePath: Path to the referenced object
76
+ * - targetPath: Path where the reference should be placed
77
+ *
78
+ * This is reset at the start of each decode() call.
79
+ *
80
+ * @type {Array<[string[], string[]]>}
81
+ * @private
82
+ */
83
+ const { getPlugin } = require("./plugins");
84
+
85
+ let pointers2Res = [];
86
+
87
+ /**
88
+ * Tag decoder lookup table
89
+ *
90
+ * Maps single-character tags to their decoder functions.
91
+ * Each decoder takes the encoded value and returns the decoded JavaScript value.
92
+ *
93
+ * @type {Object.<string, function(any, string[]?): any>}
94
+ * @private
95
+ *
96
+ * @property {function(string): RegExp} R - Decode RegExp from string pattern
97
+ * @property {function(number): Date} D - Decode Date from timestamp
98
+ * @property {function(string[], string[]): null} P - Register pointer for later resolution
99
+ * @property {function([string, string, string]): Error} E - Decode Error with name, message, stack
100
+ * @property {function(): undefined} U - Return undefined
101
+ * @property {function(any[]): Set} S - Decode Set from array
102
+ * @property {function(Object): Map} M - Decode Map from object entries
103
+ */
104
+ const tagLookup = {
105
+ /**
106
+ * Decode RegExp from string pattern
107
+ * @param {string} s - RegExp string (e.g., '/pattern/flags')
108
+ * @returns {RegExp} Reconstructed RegExp instance
109
+ */
110
+ R: (s) => {
111
+ // Parse /pattern/flags format
112
+ const match = s.match(/^\/(.*)\/([gimsuy]*)$/);
113
+ if (match) {
114
+ return new RegExp(match[1], match[2]);
115
+ }
116
+ // Fallback: treat as raw pattern with no flags
117
+ return new RegExp(s);
118
+ },
119
+
120
+ /**
121
+ * Decode Date from timestamp
122
+ * @param {number} n - Unix timestamp in milliseconds
123
+ * @returns {Date} Reconstructed Date instance
124
+ */
125
+ D: (n) => new Date(n),
126
+
127
+ /**
128
+ * Register circular reference pointer for later resolution
129
+ * @param {string[]} sourcePath - Path to the referenced object
130
+ * @param {string[]} currentPath - Path where reference should be placed
131
+ * @returns {null} Placeholder (will be replaced during resolution)
132
+ */
133
+ P: (sourcePath, currentPath) => {
134
+ pointers2Res.push([sourcePath, currentPath]);
135
+ return null;
136
+ },
137
+
138
+ /**
139
+ * Decode Error with preserved type, message, and stack
140
+ *
141
+ * Attempts to reconstruct the original error type (TypeError, RangeError, etc.)
142
+ * Falls back to generic Error if the type is not available globally.
143
+ *
144
+ * @param {[string, string, string]} errorData - Tuple of [name, message, stack]
145
+ * @returns {Error} Reconstructed Error instance
146
+ */
147
+ E: ([name, message, stack]) => {
148
+ let err;
149
+ try {
150
+ // Try to create the specific error type (TypeError, RangeError, etc.)
151
+ err = new global[name](message);
152
+ if (err instanceof Error) {
153
+ err.stack = stack;
154
+ } else {
155
+ throw {}; // Force fallback if not a real Error
156
+ }
157
+ } catch (e) {
158
+ // Fallback to generic Error with custom name
159
+ err = new Error(message);
160
+ err.name = name;
161
+ err.stack = stack;
162
+ }
163
+ return err;
164
+ },
165
+
166
+ /**
167
+ * Decode undefined value
168
+ * @returns {undefined}
169
+ */
170
+ U: () => undefined,
171
+
172
+ /**
173
+ * Decode Set from array
174
+ * @param {any[]} a - Array of set elements
175
+ * @returns {Set} Reconstructed Set instance
176
+ */
177
+ S: (a) => new Set(a),
178
+
179
+ /**
180
+ * Decode Map from object entries
181
+ * @param {Object} o - Object with key-value pairs
182
+ * @returns {Map} Reconstructed Map instance
183
+ */
184
+ M: (o) => new Map(Object.entries(o)),
185
+
186
+ /**
187
+ * Decode inline base64 binary data
188
+ * @param {string} s - Base64 encoded string
189
+ * @returns {Buffer|ArrayBuffer} Decoded binary data (Buffer in Node, ArrayBuffer in browser)
190
+ */
191
+ I: (s) => {
192
+ // In Node.js, return Buffer
193
+ if (typeof Buffer !== "undefined") {
194
+ return Buffer.from(s, "base64");
195
+ }
196
+ // In browser, return ArrayBuffer
197
+ const binaryStr = atob(s);
198
+ const bytes = new Uint8Array(binaryStr.length);
199
+ for (let i = 0; i < binaryStr.length; i++) {
200
+ bytes[i] = binaryStr.charCodeAt(i);
201
+ }
202
+ return bytes.buffer;
203
+ },
204
+ };
205
+
206
+ /**
207
+ * Parse a tagged key to extract the property name and tag
208
+ *
209
+ * JSS encodes type information in property keys using the format `name<!tag>`.
210
+ * This function separates the original property name from its type tag.
211
+ *
212
+ * Also handles array type tags which have the format `[tag1,tag2,...]`.
213
+ *
214
+ * @param {string} key - Property key potentially containing a tag
215
+ * @returns {[string, string|undefined]} Tuple of [propertyName, tag]
216
+ * Tag is undefined if key has no tag
217
+ * @private
218
+ *
219
+ * @example
220
+ * parseKeyWithTags('createdAt<!D>') // ['createdAt', 'D']
221
+ * parseKeyWithTags('pattern<!R>') // ['pattern', 'R']
222
+ * parseKeyWithTags('name') // ['name', undefined]
223
+ * parseKeyWithTags('items<![D,D,D]') // ['items', '[D,D,D]']
224
+ */
225
+ function parseKeyWithTags(key) {
226
+ const match = key.match(/(.+)<!(.*)>/);
227
+
228
+ if (match) {
229
+ const name = match[1];
230
+ let tag = match[2];
231
+
232
+ // Handle array type tags that may be split across chunks
233
+ // e.g., '[D,D,D' needs the closing ']'
234
+ if (tag.startsWith("[") && !tag.endsWith("]")) {
235
+ tag += "]";
236
+ }
237
+
238
+ return [name, tag];
239
+ }
240
+
241
+ return [key, undefined];
242
+ }
243
+
244
+ /**
245
+ * Recursively decode a value based on its tag
246
+ *
247
+ * This is the core decoding function that handles all JSS types.
248
+ * It processes:
249
+ * - Tagged values using the tagLookup decoders
250
+ * - Arrays (including typed arrays with per-element tags)
251
+ * - Objects (recursively decoding nested properties)
252
+ * - Primitive values (passed through unchanged)
253
+ *
254
+ * @param {any} val - The value to decode
255
+ * @param {string|undefined} tag - Type tag (D, R, E, U, M, S, P, or array format)
256
+ * @param {string[]} [path=[]] - Current path for circular reference tracking
257
+ * @returns {any} The decoded value with original JavaScript type
258
+ * @private
259
+ *
260
+ * @example
261
+ * // Decode a Date
262
+ * decodeValue(1704067200000, 'D')
263
+ * // Returns: Date instance
264
+ *
265
+ * @example
266
+ * // Decode a nested object
267
+ * decodeValue({ "name<!>": "test", "date<!D>": 1704067200000 }, undefined)
268
+ * // Returns: { name: 'test', date: Date }
269
+ *
270
+ * @example
271
+ * // Decode a typed array
272
+ * decodeValue([1704067200000, 1704153600000], '[D,D]')
273
+ * // Returns: [Date, Date]
274
+ */
275
+ function decodeValue(val, tag, path = []) {
276
+ // If we have a known tag, use the appropriate decoder
277
+ if (tag in tagLookup) {
278
+ return tagLookup[tag](val, path);
279
+ }
280
+
281
+ // Check custom plugins
282
+ const plugin = getPlugin(tag);
283
+ if (plugin) {
284
+ return plugin.decode(val, path, {});
285
+ }
286
+
287
+ // Handle arrays
288
+ if (Array.isArray(val)) {
289
+ const res = [];
290
+
291
+ // Check if this is a typed array (tag format: '[D,D,D]')
292
+ const isTaggedArray = tag && tag.startsWith("[");
293
+ const typeTags = isTaggedArray ? tag.slice(1, -1).split(",") : [];
294
+
295
+ for (let i = 0; i < val.length; i++) {
296
+ res.push(decodeValue(val[i], typeTags[i], [...path, i]));
297
+ }
298
+
299
+ return res;
300
+ }
301
+
302
+ // Handle objects
303
+ if (val !== null && typeof val === "object") {
304
+ const res = {};
305
+
306
+ for (const key in val) {
307
+ const [name, t] = parseKeyWithTags(key);
308
+ res[name] = decodeValue(val[key], t, [...path, name]);
309
+ }
310
+
311
+ return res;
312
+ }
313
+
314
+ // Primitive values - return as-is
315
+ return val;
316
+ }
317
+
318
+ /**
319
+ * Resolve a circular reference pointer
320
+ *
321
+ * After initial decoding, circular references are represented as null values
322
+ * with their paths stored in pointers2Res. This function resolves each pointer
323
+ * by navigating to the referenced object and placing it at the target location.
324
+ *
325
+ * @param {Object} obj - The root decoded object
326
+ * @param {[string[], string[]]} pointerInfo - Tuple of [refPath, attrPath]
327
+ * - refPath: Path to the object being referenced
328
+ * - attrPath: Path where the reference should be placed
329
+ * @returns {void} Modifies obj in place
330
+ * @private
331
+ *
332
+ * @example
333
+ * // Given: obj = { child: { name: 'test' }, ref: null }
334
+ * // With pointer: [['child'], ['ref']]
335
+ * // After resolution: obj.ref === obj.child
336
+ *
337
+ * resolvePointers(obj, [['child'], ['ref']])
338
+ * console.log(obj.ref === obj.child) // true
339
+ */
340
+ function resolvePointers(obj, [refPath, attrPath]) {
341
+ // Navigate to the referenced object
342
+ let ref = obj;
343
+ for (const key of refPath) {
344
+ ref = ref[key];
345
+ }
346
+
347
+ // Navigate to the parent of the target location
348
+ let attrParent = obj;
349
+ for (let i = 0; i < attrPath.length - 1; i++) {
350
+ attrParent = attrParent[attrPath[i]];
351
+ }
352
+
353
+ // Set the reference at the target location
354
+ attrParent[attrPath[attrPath.length - 1]] = ref;
355
+ }
356
+
357
+ /**
358
+ * Decode a JSS-encoded object back to its original form
359
+ *
360
+ * This is the low-level decode function that operates on already-parsed
361
+ * JavaScript objects. Use `parse()` if you have a JSON string.
362
+ *
363
+ * ## Processing Steps
364
+ *
365
+ * 1. Reset pointer storage for circular references
366
+ * 2. Recursively decode all values using decodeValue()
367
+ * 3. Resolve all circular reference pointers
368
+ * 4. Return the fully restored object
369
+ *
370
+ * @param {Object} data - JSS-encoded plain object (already parsed from JSON)
371
+ * @returns {any} Decoded object with original JavaScript types restored
372
+ *
373
+ * @example
374
+ * // Decode a Date
375
+ * const decoded = decode({ "created<!D>": 1704067200000 })
376
+ * console.log(decoded.created instanceof Date) // true
377
+ *
378
+ * @example
379
+ * // Decode multiple types
380
+ * const decoded = decode({
381
+ * "date<!D>": 1704067200000,
382
+ * "regex<!R>": "/test/gi",
383
+ * "items<!S>": [1, 2, 3],
384
+ * "config<!M>": { key: "value" }
385
+ * })
386
+ *
387
+ * console.log(decoded.date instanceof Date) // true
388
+ * console.log(decoded.regex instanceof RegExp) // true
389
+ * console.log(decoded.items instanceof Set) // true
390
+ * console.log(decoded.config instanceof Map) // true
391
+ *
392
+ * @example
393
+ * // Decode with circular reference
394
+ * const decoded = decode({
395
+ * name: "root",
396
+ * "self<!P>": [] // Pointer to root
397
+ * })
398
+ *
399
+ * console.log(decoded.self === decoded) // true
400
+ */
401
+ function decode(data) {
402
+ // Reset pointer storage for this decode operation
403
+ pointers2Res = [];
404
+
405
+ // Decode all values recursively
406
+ const result = decodeValue(data, undefined, []);
407
+
408
+ // Resolve all circular reference pointers
409
+ pointers2Res.forEach((p) => resolvePointers(result, p));
410
+
411
+ return result;
412
+ }
413
+
414
+ /**
415
+ * Parse a JSS-encoded JSON string back to its original form
416
+ *
417
+ * This is the high-level parse function that combines JSON.parse with
418
+ * JSS decoding. It's the counterpart to `stringify()` from the encode module.
419
+ *
420
+ * ## Usage
421
+ *
422
+ * ```javascript
423
+ * const { parse } = require('./decode')
424
+ * const original = parse(jssString)
425
+ * ```
426
+ *
427
+ * ## Error Handling
428
+ *
429
+ * - Throws `SyntaxError` if the string is not valid JSON
430
+ * - Invalid tags are silently ignored (value passed through as-is)
431
+ * - Missing referenced objects in pointers will cause runtime errors
432
+ *
433
+ * @param {string} encoded - JSS-encoded JSON string
434
+ * @returns {any} Decoded object with original JavaScript types restored
435
+ * @throws {SyntaxError} If the input is not valid JSON
436
+ *
437
+ * @example
438
+ * // Parse a complete JSS message
439
+ * const result = parse(`{
440
+ * "type": "message",
441
+ * "timestamp<!D>": 1704067200000,
442
+ * "pattern<!R>": "/hello/i",
443
+ * "data": {
444
+ * "items<!S>": [1, 2, 3]
445
+ * }
446
+ * }`)
447
+ *
448
+ * console.log(result.type) // 'message'
449
+ * console.log(result.timestamp instanceof Date) // true
450
+ * console.log(result.pattern instanceof RegExp) // true
451
+ * console.log(result.data.items instanceof Set) // true
452
+ *
453
+ * @example
454
+ * // Round-trip with encode
455
+ * const { stringify } = require('./encode')
456
+ * const { parse } = require('./decode')
457
+ *
458
+ * const original = {
459
+ * date: new Date(),
460
+ * items: new Set([1, 2, 3])
461
+ * }
462
+ *
463
+ * const restored = parse(stringify(original))
464
+ * console.log(restored.date.getTime() === original.date.getTime()) // true
465
+ * console.log([...restored.items]) // [1, 2, 3]
466
+ */
467
+ function parse(encoded) {
468
+ return decode(JSON.parse(encoded));
469
+ }
470
+
471
+ module.exports = { decode, parse };