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.
- package/README.md +58 -570
- package/client/README.md +73 -14
- package/client/auth/crypto/aead.js +214 -0
- package/client/auth/crypto/constants.js +32 -0
- package/client/auth/crypto/encoding.js +104 -0
- package/client/auth/crypto/files.md +27 -0
- package/client/auth/crypto/kdf.js +217 -0
- package/client/auth/crypto-utils.js +118 -0
- package/client/auth/files.md +52 -0
- package/client/auth/key-recovery.js +288 -0
- package/client/auth/recovery/constants.js +37 -0
- package/client/auth/recovery/files.md +23 -0
- package/client/auth/recovery/key-derivation.js +61 -0
- package/client/auth/recovery/sss-browser.js +189 -0
- package/client/auth/share-storage.js +205 -0
- package/client/auth/storage/constants.js +18 -0
- package/client/auth/storage/db.js +132 -0
- package/client/auth/storage/files.md +27 -0
- package/client/auth/storage/keys.js +173 -0
- package/client/auth/storage/shares.js +200 -0
- package/client/browser.js +190 -23
- package/client/connectSocket.js +418 -988
- package/client/connection/README.md +23 -0
- package/client/connection/fileDownload.js +256 -0
- package/client/connection/fileHandling.js +450 -0
- package/client/connection/fileUtils.js +346 -0
- package/client/connection/files.md +71 -0
- package/client/connection/messageHandler.js +105 -0
- package/client/connection/network.js +350 -0
- package/client/connection/proxy.js +233 -0
- package/client/connection/sender.js +333 -0
- package/client/connection/state.js +321 -0
- package/client/connection/subscriptions.js +151 -0
- package/client/files.md +53 -0
- package/client/index.js +298 -142
- package/client/transports/README.md +50 -0
- package/client/transports/files.md +41 -0
- package/client/transports/streamParser.js +195 -0
- package/client/transports/streaming.js +555 -202
- package/dist/ape.js +6 -1
- package/dist/ape.js.map +4 -4
- package/index.d.ts +38 -16
- package/package.json +32 -7
- package/server/README.md +287 -53
- package/server/adapters/README.md +28 -19
- package/server/adapters/files.md +68 -0
- package/server/adapters/firebase.js +543 -160
- package/server/adapters/index.js +362 -112
- package/server/adapters/mongo.js +530 -140
- package/server/adapters/postgres.js +534 -155
- package/server/adapters/redis.js +508 -143
- package/server/adapters/supabase.js +555 -186
- package/server/client/README.md +43 -0
- package/server/client/connection.js +586 -0
- package/server/client/files.md +40 -0
- package/server/client/index.js +342 -0
- package/server/files.md +54 -0
- package/server/index.js +332 -27
- package/server/lib/README.md +26 -0
- package/server/lib/broadcast/clients.js +219 -0
- package/server/lib/broadcast/files.md +58 -0
- package/server/lib/broadcast/index.js +57 -0
- package/server/lib/broadcast/publishProxy.js +110 -0
- package/server/lib/broadcast/pubsub.js +137 -0
- package/server/lib/broadcast/sendProxy.js +103 -0
- package/server/lib/bun.js +315 -99
- package/server/lib/fileTransfer/README.md +63 -0
- package/server/lib/fileTransfer/files.md +30 -0
- package/server/lib/fileTransfer/streaming.js +435 -0
- package/server/lib/fileTransfer.js +710 -326
- package/server/lib/files.md +111 -0
- package/server/lib/httpUtils.js +283 -0
- package/server/lib/loader.js +208 -7
- package/server/lib/longPolling/README.md +63 -0
- package/server/lib/longPolling/files.md +44 -0
- package/server/lib/longPolling/getHandler.js +365 -0
- package/server/lib/longPolling/postHandler.js +327 -0
- package/server/lib/longPolling.js +174 -221
- package/server/lib/main.js +369 -532
- package/server/lib/runtimes/README.md +42 -0
- package/server/lib/runtimes/bun.js +586 -0
- package/server/lib/runtimes/files.md +56 -0
- package/server/lib/runtimes/node.js +511 -0
- package/server/lib/wiring.js +539 -98
- package/server/lib/ws/README.md +35 -0
- package/server/lib/ws/adapters/README.md +54 -0
- package/server/lib/ws/adapters/bun.js +538 -170
- package/server/lib/ws/adapters/deno.js +623 -149
- package/server/lib/ws/adapters/files.md +42 -0
- package/server/lib/ws/files.md +74 -0
- package/server/lib/ws/frames.js +532 -154
- package/server/lib/ws/index.js +207 -10
- package/server/lib/ws/server.js +385 -92
- package/server/lib/ws/socket.js +549 -181
- package/server/lib/wsProvider.js +363 -89
- package/server/plugins/binary.js +282 -0
- package/server/security/README.md +92 -0
- package/server/security/auth/README.md +319 -0
- package/server/security/auth/adapters/files.md +95 -0
- package/server/security/auth/adapters/ldap/constants.js +37 -0
- package/server/security/auth/adapters/ldap/files.md +19 -0
- package/server/security/auth/adapters/ldap/helpers.js +111 -0
- package/server/security/auth/adapters/ldap.js +353 -0
- package/server/security/auth/adapters/oauth2/constants.js +41 -0
- package/server/security/auth/adapters/oauth2/files.md +19 -0
- package/server/security/auth/adapters/oauth2/helpers.js +123 -0
- package/server/security/auth/adapters/oauth2.js +273 -0
- package/server/security/auth/adapters/opaque-handlers.js +314 -0
- package/server/security/auth/adapters/opaque.js +205 -0
- package/server/security/auth/adapters/saml/constants.js +52 -0
- package/server/security/auth/adapters/saml/files.md +19 -0
- package/server/security/auth/adapters/saml/helpers.js +74 -0
- package/server/security/auth/adapters/saml.js +173 -0
- package/server/security/auth/adapters/totp.js +703 -0
- package/server/security/auth/adapters/webauthn.js +625 -0
- package/server/security/auth/files.md +61 -0
- package/server/security/auth/framework/constants.js +27 -0
- package/server/security/auth/framework/files.md +23 -0
- package/server/security/auth/framework/handlers.js +272 -0
- package/server/security/auth/framework/socket-auth.js +177 -0
- package/server/security/auth/handlers/auth-messages.js +143 -0
- package/server/security/auth/handlers/files.md +28 -0
- package/server/security/auth/index.js +290 -0
- package/server/security/auth/mfa/crypto/aead.js +148 -0
- package/server/security/auth/mfa/crypto/constants.js +35 -0
- package/server/security/auth/mfa/crypto/files.md +27 -0
- package/server/security/auth/mfa/crypto/kdf.js +120 -0
- package/server/security/auth/mfa/crypto/utils.js +68 -0
- package/server/security/auth/mfa/crypto-utils.js +80 -0
- package/server/security/auth/mfa/files.md +77 -0
- package/server/security/auth/mfa/ledger/constants.js +75 -0
- package/server/security/auth/mfa/ledger/errors.js +73 -0
- package/server/security/auth/mfa/ledger/files.md +23 -0
- package/server/security/auth/mfa/ledger/share-record.js +32 -0
- package/server/security/auth/mfa/ledger.js +255 -0
- package/server/security/auth/mfa/recovery/constants.js +67 -0
- package/server/security/auth/mfa/recovery/files.md +19 -0
- package/server/security/auth/mfa/recovery/handlers.js +216 -0
- package/server/security/auth/mfa/recovery.js +191 -0
- package/server/security/auth/mfa/sss/constants.js +21 -0
- package/server/security/auth/mfa/sss/files.md +23 -0
- package/server/security/auth/mfa/sss/gf256.js +103 -0
- package/server/security/auth/mfa/sss/serialization.js +82 -0
- package/server/security/auth/mfa/sss.js +161 -0
- package/server/security/auth/mfa/two-of-three/constants.js +58 -0
- package/server/security/auth/mfa/two-of-three/files.md +23 -0
- package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
- package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
- package/server/security/auth/mfa/two-of-three.js +136 -0
- package/server/security/auth/nonce-manager.js +89 -0
- package/server/security/auth/state-machine-mfa.js +269 -0
- package/server/security/auth/state-machine.js +257 -0
- package/server/security/extractRootDomain.js +144 -16
- package/server/security/files.md +51 -0
- package/server/security/origin.js +197 -15
- package/server/security/reply.js +274 -16
- package/server/socket/README.md +119 -0
- package/server/socket/authMiddleware.js +299 -0
- package/server/socket/files.md +86 -0
- package/server/socket/open.js +154 -8
- package/server/socket/pluginHooks.js +334 -0
- package/server/socket/receive.js +184 -225
- package/server/socket/receiveContext.js +117 -0
- package/server/socket/send.js +416 -78
- package/server/socket/tagUtils.js +402 -0
- package/server/utils/README.md +19 -0
- package/server/utils/deepRequire.js +255 -30
- package/server/utils/files.md +57 -0
- package/server/utils/genId.js +182 -20
- package/server/utils/parseUserAgent.js +313 -251
- package/server/utils/userAgent/README.md +65 -0
- package/server/utils/userAgent/files.md +46 -0
- package/server/utils/userAgent/patterns.js +545 -0
- package/utils/README.md +21 -0
- package/utils/files.md +66 -0
- package/utils/jss/README.md +21 -0
- package/utils/jss/decode.js +471 -0
- package/utils/jss/encode.js +312 -0
- package/utils/jss/files.md +68 -0
- package/utils/jss/plugins.js +210 -0
- package/utils/jss.js +219 -273
- package/utils/messageHash.js +238 -35
- package/dist/api-ape.min.js +0 -2
- package/dist/api-ape.min.js.map +0 -7
- package/server/client.js +0 -308
- 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
|
+
};
|