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.
- package/README.md +59 -572
- 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 -203
- package/dist/ape.js +6 -1
- package/dist/ape.js.map +4 -4
- package/index.d.ts +38 -16
- package/package.json +31 -6
- package/server/README.md +272 -67
- package/server/adapters/README.md +23 -14
- 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 +322 -71
- 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 -219
- 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 -224
- 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 -311
- 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 };
|