api-ape 3.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +58 -570
  2. package/client/README.md +73 -14
  3. package/client/auth/crypto/aead.js +214 -0
  4. package/client/auth/crypto/constants.js +32 -0
  5. package/client/auth/crypto/encoding.js +104 -0
  6. package/client/auth/crypto/files.md +27 -0
  7. package/client/auth/crypto/kdf.js +217 -0
  8. package/client/auth/crypto-utils.js +118 -0
  9. package/client/auth/files.md +52 -0
  10. package/client/auth/key-recovery.js +288 -0
  11. package/client/auth/recovery/constants.js +37 -0
  12. package/client/auth/recovery/files.md +23 -0
  13. package/client/auth/recovery/key-derivation.js +61 -0
  14. package/client/auth/recovery/sss-browser.js +189 -0
  15. package/client/auth/share-storage.js +205 -0
  16. package/client/auth/storage/constants.js +18 -0
  17. package/client/auth/storage/db.js +132 -0
  18. package/client/auth/storage/files.md +27 -0
  19. package/client/auth/storage/keys.js +173 -0
  20. package/client/auth/storage/shares.js +200 -0
  21. package/client/browser.js +190 -23
  22. package/client/connectSocket.js +418 -988
  23. package/client/connection/README.md +23 -0
  24. package/client/connection/fileDownload.js +256 -0
  25. package/client/connection/fileHandling.js +450 -0
  26. package/client/connection/fileUtils.js +346 -0
  27. package/client/connection/files.md +71 -0
  28. package/client/connection/messageHandler.js +105 -0
  29. package/client/connection/network.js +350 -0
  30. package/client/connection/proxy.js +233 -0
  31. package/client/connection/sender.js +333 -0
  32. package/client/connection/state.js +321 -0
  33. package/client/connection/subscriptions.js +151 -0
  34. package/client/files.md +53 -0
  35. package/client/index.js +298 -142
  36. package/client/transports/README.md +50 -0
  37. package/client/transports/files.md +41 -0
  38. package/client/transports/streamParser.js +195 -0
  39. package/client/transports/streaming.js +555 -202
  40. package/dist/ape.js +6 -1
  41. package/dist/ape.js.map +4 -4
  42. package/index.d.ts +38 -16
  43. package/package.json +32 -7
  44. package/server/README.md +287 -53
  45. package/server/adapters/README.md +28 -19
  46. package/server/adapters/files.md +68 -0
  47. package/server/adapters/firebase.js +543 -160
  48. package/server/adapters/index.js +362 -112
  49. package/server/adapters/mongo.js +530 -140
  50. package/server/adapters/postgres.js +534 -155
  51. package/server/adapters/redis.js +508 -143
  52. package/server/adapters/supabase.js +555 -186
  53. package/server/client/README.md +43 -0
  54. package/server/client/connection.js +586 -0
  55. package/server/client/files.md +40 -0
  56. package/server/client/index.js +342 -0
  57. package/server/files.md +54 -0
  58. package/server/index.js +332 -27
  59. package/server/lib/README.md +26 -0
  60. package/server/lib/broadcast/clients.js +219 -0
  61. package/server/lib/broadcast/files.md +58 -0
  62. package/server/lib/broadcast/index.js +57 -0
  63. package/server/lib/broadcast/publishProxy.js +110 -0
  64. package/server/lib/broadcast/pubsub.js +137 -0
  65. package/server/lib/broadcast/sendProxy.js +103 -0
  66. package/server/lib/bun.js +315 -99
  67. package/server/lib/fileTransfer/README.md +63 -0
  68. package/server/lib/fileTransfer/files.md +30 -0
  69. package/server/lib/fileTransfer/streaming.js +435 -0
  70. package/server/lib/fileTransfer.js +710 -326
  71. package/server/lib/files.md +111 -0
  72. package/server/lib/httpUtils.js +283 -0
  73. package/server/lib/loader.js +208 -7
  74. package/server/lib/longPolling/README.md +63 -0
  75. package/server/lib/longPolling/files.md +44 -0
  76. package/server/lib/longPolling/getHandler.js +365 -0
  77. package/server/lib/longPolling/postHandler.js +327 -0
  78. package/server/lib/longPolling.js +174 -221
  79. package/server/lib/main.js +369 -532
  80. package/server/lib/runtimes/README.md +42 -0
  81. package/server/lib/runtimes/bun.js +586 -0
  82. package/server/lib/runtimes/files.md +56 -0
  83. package/server/lib/runtimes/node.js +511 -0
  84. package/server/lib/wiring.js +539 -98
  85. package/server/lib/ws/README.md +35 -0
  86. package/server/lib/ws/adapters/README.md +54 -0
  87. package/server/lib/ws/adapters/bun.js +538 -170
  88. package/server/lib/ws/adapters/deno.js +623 -149
  89. package/server/lib/ws/adapters/files.md +42 -0
  90. package/server/lib/ws/files.md +74 -0
  91. package/server/lib/ws/frames.js +532 -154
  92. package/server/lib/ws/index.js +207 -10
  93. package/server/lib/ws/server.js +385 -92
  94. package/server/lib/ws/socket.js +549 -181
  95. package/server/lib/wsProvider.js +363 -89
  96. package/server/plugins/binary.js +282 -0
  97. package/server/security/README.md +92 -0
  98. package/server/security/auth/README.md +319 -0
  99. package/server/security/auth/adapters/files.md +95 -0
  100. package/server/security/auth/adapters/ldap/constants.js +37 -0
  101. package/server/security/auth/adapters/ldap/files.md +19 -0
  102. package/server/security/auth/adapters/ldap/helpers.js +111 -0
  103. package/server/security/auth/adapters/ldap.js +353 -0
  104. package/server/security/auth/adapters/oauth2/constants.js +41 -0
  105. package/server/security/auth/adapters/oauth2/files.md +19 -0
  106. package/server/security/auth/adapters/oauth2/helpers.js +123 -0
  107. package/server/security/auth/adapters/oauth2.js +273 -0
  108. package/server/security/auth/adapters/opaque-handlers.js +314 -0
  109. package/server/security/auth/adapters/opaque.js +205 -0
  110. package/server/security/auth/adapters/saml/constants.js +52 -0
  111. package/server/security/auth/adapters/saml/files.md +19 -0
  112. package/server/security/auth/adapters/saml/helpers.js +74 -0
  113. package/server/security/auth/adapters/saml.js +173 -0
  114. package/server/security/auth/adapters/totp.js +703 -0
  115. package/server/security/auth/adapters/webauthn.js +625 -0
  116. package/server/security/auth/files.md +61 -0
  117. package/server/security/auth/framework/constants.js +27 -0
  118. package/server/security/auth/framework/files.md +23 -0
  119. package/server/security/auth/framework/handlers.js +272 -0
  120. package/server/security/auth/framework/socket-auth.js +177 -0
  121. package/server/security/auth/handlers/auth-messages.js +143 -0
  122. package/server/security/auth/handlers/files.md +28 -0
  123. package/server/security/auth/index.js +290 -0
  124. package/server/security/auth/mfa/crypto/aead.js +148 -0
  125. package/server/security/auth/mfa/crypto/constants.js +35 -0
  126. package/server/security/auth/mfa/crypto/files.md +27 -0
  127. package/server/security/auth/mfa/crypto/kdf.js +120 -0
  128. package/server/security/auth/mfa/crypto/utils.js +68 -0
  129. package/server/security/auth/mfa/crypto-utils.js +80 -0
  130. package/server/security/auth/mfa/files.md +77 -0
  131. package/server/security/auth/mfa/ledger/constants.js +75 -0
  132. package/server/security/auth/mfa/ledger/errors.js +73 -0
  133. package/server/security/auth/mfa/ledger/files.md +23 -0
  134. package/server/security/auth/mfa/ledger/share-record.js +32 -0
  135. package/server/security/auth/mfa/ledger.js +255 -0
  136. package/server/security/auth/mfa/recovery/constants.js +67 -0
  137. package/server/security/auth/mfa/recovery/files.md +19 -0
  138. package/server/security/auth/mfa/recovery/handlers.js +216 -0
  139. package/server/security/auth/mfa/recovery.js +191 -0
  140. package/server/security/auth/mfa/sss/constants.js +21 -0
  141. package/server/security/auth/mfa/sss/files.md +23 -0
  142. package/server/security/auth/mfa/sss/gf256.js +103 -0
  143. package/server/security/auth/mfa/sss/serialization.js +82 -0
  144. package/server/security/auth/mfa/sss.js +161 -0
  145. package/server/security/auth/mfa/two-of-three/constants.js +58 -0
  146. package/server/security/auth/mfa/two-of-three/files.md +23 -0
  147. package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
  148. package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
  149. package/server/security/auth/mfa/two-of-three.js +136 -0
  150. package/server/security/auth/nonce-manager.js +89 -0
  151. package/server/security/auth/state-machine-mfa.js +269 -0
  152. package/server/security/auth/state-machine.js +257 -0
  153. package/server/security/extractRootDomain.js +144 -16
  154. package/server/security/files.md +51 -0
  155. package/server/security/origin.js +197 -15
  156. package/server/security/reply.js +274 -16
  157. package/server/socket/README.md +119 -0
  158. package/server/socket/authMiddleware.js +299 -0
  159. package/server/socket/files.md +86 -0
  160. package/server/socket/open.js +154 -8
  161. package/server/socket/pluginHooks.js +334 -0
  162. package/server/socket/receive.js +184 -225
  163. package/server/socket/receiveContext.js +117 -0
  164. package/server/socket/send.js +416 -78
  165. package/server/socket/tagUtils.js +402 -0
  166. package/server/utils/README.md +19 -0
  167. package/server/utils/deepRequire.js +255 -30
  168. package/server/utils/files.md +57 -0
  169. package/server/utils/genId.js +182 -20
  170. package/server/utils/parseUserAgent.js +313 -251
  171. package/server/utils/userAgent/README.md +65 -0
  172. package/server/utils/userAgent/files.md +46 -0
  173. package/server/utils/userAgent/patterns.js +545 -0
  174. package/utils/README.md +21 -0
  175. package/utils/files.md +66 -0
  176. package/utils/jss/README.md +21 -0
  177. package/utils/jss/decode.js +471 -0
  178. package/utils/jss/encode.js +312 -0
  179. package/utils/jss/files.md +68 -0
  180. package/utils/jss/plugins.js +210 -0
  181. package/utils/jss.js +219 -273
  182. package/utils/messageHash.js +238 -35
  183. package/dist/api-ape.min.js +0 -2
  184. package/dist/api-ape.min.js.map +0 -7
  185. package/server/client.js +0 -308
  186. package/server/lib/broadcast.js +0 -146
@@ -0,0 +1,23 @@
1
+ # Client Auth Recovery Module
2
+
3
+ Browser-side key recovery using Shamir Secret Sharing.
4
+
5
+ ## Directory Structure
6
+
7
+ ```
8
+ recovery/
9
+ ├── constants.js - Recovery error codes and factor types
10
+ ├── key-derivation.js - Per-factor key derivation (S1, S2, S3)
11
+ └── sss-browser.js - Shamir Secret Sharing for browser (GF256)
12
+ ```
13
+
14
+ ## Files
15
+
16
+ ### `constants.js`
17
+ Defines KeyRecoveryError codes and FactorType enum (OAUTH, WEBAUTHN, TOTP).
18
+
19
+ ### `key-derivation.js`
20
+ Derives encryption keys for each share: deriveS1Key (OAuth), deriveS2Key (WebAuthn), deriveS3Key (TOTP).
21
+
22
+ ### `sss-browser.js`
23
+ Implements Shamir Secret Sharing over GF(256) for browsers. Includes Lagrange interpolation, share serialization, and Galois field arithmetic.
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @file Factor-specific key derivation functions
3
+ */
4
+ 'use strict';
5
+
6
+ const cryptoUtils = require('../crypto-utils');
7
+
8
+ /**
9
+ * Derive S1 encryption key from OAuth token
10
+ * @param {string} oauthToken - OAuth access token
11
+ * @param {string} userId - User identifier
12
+ * @returns {Promise<Uint8Array>}
13
+ */
14
+ async function deriveS1Key(oauthToken, userId) {
15
+ return cryptoUtils.hkdf(
16
+ cryptoUtils.stringToUint8Array(oauthToken),
17
+ cryptoUtils.stringToUint8Array(`api-ape:s1:${userId}`),
18
+ cryptoUtils.stringToUint8Array('S1_key'),
19
+ 32
20
+ );
21
+ }
22
+
23
+ /**
24
+ * Derive S2 encryption key from WebAuthn data
25
+ * @param {Uint8Array} authenticatorData - WebAuthn data
26
+ * @param {string} credentialId - Credential ID
27
+ * @returns {Promise<Uint8Array>}
28
+ */
29
+ async function deriveS2Key(authenticatorData, credentialId) {
30
+ return cryptoUtils.hkdf(
31
+ authenticatorData,
32
+ cryptoUtils.stringToUint8Array(credentialId),
33
+ cryptoUtils.stringToUint8Array('S2_key'),
34
+ 32
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Derive S3 encryption key from TOTP seed
40
+ * @param {string} totpSeed - TOTP seed
41
+ * @param {Uint8Array} salt - Salt value
42
+ * @returns {Promise<Uint8Array>}
43
+ */
44
+ async function deriveS3Key(totpSeed, salt) {
45
+ return cryptoUtils.argon2id(
46
+ cryptoUtils.stringToUint8Array(totpSeed),
47
+ salt,
48
+ {
49
+ memoryCost: 65536,
50
+ timeCost: 3,
51
+ parallelism: 4,
52
+ hashLength: 32
53
+ }
54
+ );
55
+ }
56
+
57
+ module.exports = {
58
+ deriveS1Key,
59
+ deriveS2Key,
60
+ deriveS3Key,
61
+ };
@@ -0,0 +1,189 @@
1
+ /**
2
+ * @file Shamir Secret Sharing for browser (GF(256))
3
+ */
4
+ 'use strict';
5
+
6
+ const { KeyRecoveryError } = require('./constants');
7
+
8
+ /**
9
+ * Pre-computed log and exp tables for GF(256)
10
+ * @type {Object}
11
+ */
12
+ const GF256 = (function initGF256() {
13
+ const exp = new Uint8Array(512);
14
+ const log = new Uint8Array(256);
15
+
16
+ let x = 1;
17
+ for (let i = 0; i < 255; i++) {
18
+ exp[i] = x;
19
+ exp[i + 255] = x;
20
+ log[x] = i;
21
+ x = x ^ ((x << 1) ^ (x & 0x80 ? 0x11b : 0));
22
+ }
23
+ log[0] = 0;
24
+ exp[510] = exp[0];
25
+
26
+ return { exp, log };
27
+ })();
28
+
29
+ /**
30
+ * Multiply in GF(256)
31
+ * @param {number} a - First operand
32
+ * @param {number} b - Second operand
33
+ * @returns {number}
34
+ */
35
+ function gfMul(a, b) {
36
+ if (a === 0 || b === 0) return 0;
37
+ return GF256.exp[GF256.log[a] + GF256.log[b]];
38
+ }
39
+
40
+ /**
41
+ * Divide in GF(256)
42
+ * @param {number} a - Dividend
43
+ * @param {number} b - Divisor
44
+ * @returns {number}
45
+ */
46
+ function gfDiv(a, b) {
47
+ if (b === 0) throw new Error('Division by zero in GF(256)');
48
+ if (a === 0) return 0;
49
+ return GF256.exp[GF256.log[a] + 255 - GF256.log[b]];
50
+ }
51
+
52
+ /**
53
+ * Add in GF(256) (XOR)
54
+ * @param {number} a - First operand
55
+ * @param {number} b - Second operand
56
+ * @returns {number}
57
+ */
58
+ function gfAdd(a, b) {
59
+ return a ^ b;
60
+ }
61
+
62
+ /**
63
+ * Lagrange interpolation to find f(0)
64
+ * @param {Array} points - Array of point objects
65
+ * @returns {number}
66
+ */
67
+ function lagrangeInterpolate(points) {
68
+ let result = 0;
69
+
70
+ for (let i = 0; i < points.length; i++) {
71
+ let numerator = 1;
72
+ let denominator = 1;
73
+
74
+ for (let j = 0; j < points.length; j++) {
75
+ if (i !== j) {
76
+ numerator = gfMul(numerator, points[j].x);
77
+ denominator = gfMul(denominator, gfAdd(points[i].x, points[j].x));
78
+ }
79
+ }
80
+
81
+ const basis = gfMul(points[i].y, gfDiv(numerator, denominator));
82
+ result = gfAdd(result, basis);
83
+ }
84
+
85
+ return result;
86
+ }
87
+
88
+ /**
89
+ * Combine shares to reconstruct secret
90
+ * @param {Array} shares - Share objects
91
+ * @returns {Uint8Array}
92
+ */
93
+ function combineShares(shares) {
94
+ if (!Array.isArray(shares) || shares.length < 2) {
95
+ const err = new Error('At least 2 shares required');
96
+ err.code = KeyRecoveryError.INSUFFICIENT_FACTORS;
97
+ throw err;
98
+ }
99
+
100
+ const secretLength = shares[0].data.length;
101
+ const secret = new Uint8Array(secretLength);
102
+
103
+ for (let byteIdx = 0; byteIdx < secretLength; byteIdx++) {
104
+ const points = shares.map(share => ({
105
+ x: share.index,
106
+ y: share.data[byteIdx]
107
+ }));
108
+ secret[byteIdx] = lagrangeInterpolate(points);
109
+ }
110
+
111
+ return secret;
112
+ }
113
+
114
+ /**
115
+ * Deserialize share from base64url
116
+ * @param {string} serialized - Serialized share
117
+ * @returns {Object}
118
+ */
119
+ function deserializeShare(serialized) {
120
+ if (typeof serialized !== 'string' || serialized.length === 0) {
121
+ const err = new Error('Invalid serialized share');
122
+ err.code = KeyRecoveryError.INVALID_FACTOR;
123
+ throw err;
124
+ }
125
+
126
+ const base64 = serialized.replace(/-/g, '+').replace(/_/g, '/');
127
+ const binary = atob(base64);
128
+ const packed = new Uint8Array(binary.length);
129
+ for (let i = 0; i < binary.length; i++) {
130
+ packed[i] = binary.charCodeAt(i);
131
+ }
132
+
133
+ if (packed.length < 2) {
134
+ const err = new Error('Serialized share too short');
135
+ err.code = KeyRecoveryError.INVALID_FACTOR;
136
+ throw err;
137
+ }
138
+
139
+ return {
140
+ index: packed[0],
141
+ data: packed.slice(1)
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Serialize share to base64url
147
+ * @param {Object} share - Share to serialize
148
+ * @param {number} share.index - Share index
149
+ * @param {Uint8Array} share.data - Share data
150
+ * @returns {string}
151
+ */
152
+ function serializeShare(share) {
153
+ const packed = new Uint8Array(1 + share.data.length);
154
+ packed[0] = share.index;
155
+ packed.set(share.data, 1);
156
+
157
+ let binary = '';
158
+ for (let i = 0; i < packed.length; i++) {
159
+ binary += String.fromCharCode(packed[i]);
160
+ }
161
+
162
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
163
+ }
164
+
165
+ /**
166
+ * Evaluate polynomial at x using Horner's method
167
+ * @param {Uint8Array} coefficients - Polynomial coefficients
168
+ * @param {number} x - Point to evaluate
169
+ * @returns {number}
170
+ */
171
+ function evaluatePolynomial(coefficients, x) {
172
+ let result = 0;
173
+ for (let i = coefficients.length - 1; i >= 0; i--) {
174
+ result = gfAdd(gfMul(result, x), coefficients[i]);
175
+ }
176
+ return result;
177
+ }
178
+
179
+ module.exports = {
180
+ combineShares,
181
+ deserializeShare,
182
+ serializeShare,
183
+ evaluatePolynomial,
184
+ lagrangeInterpolate,
185
+ gfMul,
186
+ gfDiv,
187
+ gfAdd,
188
+ GF256,
189
+ };
@@ -0,0 +1,205 @@
1
+ /**
2
+ * @file IndexedDB Storage for S2 Share (WebAuthn-gated)
3
+ *
4
+ * Stores the encrypted S2 share and wrapped L_key locally in the browser.
5
+ * S2 is encrypted with L_key, which is derived from WebAuthn authenticator data.
6
+ *
7
+ * @module client/auth/share-storage
8
+ */
9
+ 'use strict';
10
+
11
+ const {
12
+ DB_NAME,
13
+ DB_VERSION,
14
+ STORE_SHARES,
15
+ STORE_KEYS,
16
+ STORE_METADATA,
17
+ } = require('./storage/constants');
18
+
19
+ const { openDatabase, clearDatabase } = require('./storage/db');
20
+
21
+ const {
22
+ saveShare,
23
+ getShare,
24
+ getAllShares,
25
+ deleteShare,
26
+ getShareVersion,
27
+ } = require('./storage/shares');
28
+
29
+ const {
30
+ saveWrappedKey,
31
+ getWrappedKey,
32
+ getAllWrappedKeys,
33
+ deleteWrappedKey,
34
+ } = require('./storage/keys');
35
+
36
+ /**
37
+ * Save enrollment metadata for a user
38
+ * @param {string} userId - User identifier
39
+ * @param {Object} [metadata] - Enrollment metadata
40
+ * @returns {Promise<void>}
41
+ */
42
+ async function saveMetadata(userId, metadata = {}) {
43
+ if (!userId || typeof userId !== 'string') {
44
+ throw new Error('userId is required and must be a string');
45
+ }
46
+
47
+ const db = await openDatabase();
48
+
49
+ return new Promise((resolve, reject) => {
50
+ const tx = db.transaction(STORE_METADATA, 'readwrite');
51
+ const store = tx.objectStore(STORE_METADATA);
52
+
53
+ const now = Date.now();
54
+ const record = {
55
+ userId,
56
+ enrolledAt: metadata.enrolledAt || now,
57
+ lastRecoveryAt: metadata.lastRecoveryAt || null,
58
+ shareCount: metadata.shareCount || 0,
59
+ ...metadata,
60
+ updatedAt: now
61
+ };
62
+
63
+ store.put(record);
64
+
65
+ tx.oncomplete = () => { db.close(); resolve(); };
66
+ tx.onerror = () => { db.close(); reject(tx.error); };
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Get enrollment metadata for a user
72
+ * @param {string} userId - User identifier
73
+ * @returns {Promise<Object|null>}
74
+ */
75
+ async function getMetadata(userId) {
76
+ if (!userId || typeof userId !== 'string') {
77
+ throw new Error('userId is required and must be a string');
78
+ }
79
+
80
+ const db = await openDatabase();
81
+
82
+ return new Promise((resolve, reject) => {
83
+ const tx = db.transaction(STORE_METADATA, 'readonly');
84
+ const store = tx.objectStore(STORE_METADATA);
85
+
86
+ const request = store.get(userId);
87
+
88
+ request.onsuccess = () => resolve(request.result || null);
89
+ request.onerror = () => reject(request.error);
90
+
91
+ tx.oncomplete = () => db.close();
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Delete all data for a user
97
+ * @param {string} userId - User identifier
98
+ * @returns {Promise<Object>} Count of deleted items
99
+ */
100
+ async function deleteAllUserData(userId) {
101
+ if (!userId || typeof userId !== 'string') {
102
+ throw new Error('userId is required and must be a string');
103
+ }
104
+
105
+ const db = await openDatabase();
106
+
107
+ return new Promise((resolve, reject) => {
108
+ const tx = db.transaction([STORE_SHARES, STORE_KEYS, STORE_METADATA], 'readwrite');
109
+
110
+ let sharesDeleted = 0;
111
+ let keysDeleted = 0;
112
+
113
+ const sharesStore = tx.objectStore(STORE_SHARES);
114
+ const sharesIndex = sharesStore.index('userId');
115
+ const sharesCursor = sharesIndex.openCursor(userId);
116
+
117
+ sharesCursor.onsuccess = (event) => {
118
+ const cursor = event.target.result;
119
+ if (cursor) {
120
+ sharesStore.delete(cursor.primaryKey);
121
+ sharesDeleted++;
122
+ cursor.continue();
123
+ }
124
+ };
125
+
126
+ const keysStore = tx.objectStore(STORE_KEYS);
127
+ const keysIndex = keysStore.index('userId');
128
+ const keysCursor = keysIndex.openCursor(userId);
129
+
130
+ keysCursor.onsuccess = (event) => {
131
+ const cursor = event.target.result;
132
+ if (cursor) {
133
+ keysStore.delete(cursor.primaryKey);
134
+ keysDeleted++;
135
+ cursor.continue();
136
+ }
137
+ };
138
+
139
+ const metadataStore = tx.objectStore(STORE_METADATA);
140
+ metadataStore.delete(userId);
141
+
142
+ tx.oncomplete = () => { db.close(); resolve({ shares: sharesDeleted, keys: keysDeleted }); };
143
+ tx.onerror = () => { db.close(); reject(tx.error); };
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Check if a user has enrolled
149
+ * @param {string} userId - User identifier
150
+ * @returns {Promise<boolean>}
151
+ */
152
+ async function isEnrolled(userId) {
153
+ const metadata = await getMetadata(userId);
154
+ return metadata !== null && metadata.enrolledAt !== undefined;
155
+ }
156
+
157
+ /**
158
+ * Create a storage instance with bound userId
159
+ * @param {string} userId - User identifier
160
+ * @returns {Object} Storage API bound to userId
161
+ */
162
+ function createUserStorage(userId) {
163
+ if (!userId || typeof userId !== 'string') {
164
+ throw new Error('userId is required and must be a string');
165
+ }
166
+
167
+ return {
168
+ userId,
169
+ saveShare: (shareId, encryptedShare, version) => saveShare(userId, shareId, encryptedShare, version),
170
+ getShare: (shareId) => getShare(userId, shareId),
171
+ getAllShares: () => getAllShares(userId),
172
+ deleteShare: (shareId) => deleteShare(userId, shareId),
173
+ getShareVersion: (shareId) => getShareVersion(userId, shareId),
174
+ saveWrappedKey: (keyId, wrappedKey) => saveWrappedKey(userId, keyId, wrappedKey),
175
+ getAllWrappedKeys: () => getAllWrappedKeys(userId),
176
+ saveMetadata: (metadata) => saveMetadata(userId, metadata),
177
+ getMetadata: () => getMetadata(userId),
178
+ isEnrolled: () => isEnrolled(userId),
179
+ deleteAll: () => deleteAllUserData(userId)
180
+ };
181
+ }
182
+
183
+ module.exports = {
184
+ openDatabase,
185
+ clearDatabase,
186
+ saveShare,
187
+ getShare,
188
+ getAllShares,
189
+ deleteShare,
190
+ getShareVersion,
191
+ saveWrappedKey,
192
+ getWrappedKey,
193
+ getAllWrappedKeys,
194
+ deleteWrappedKey,
195
+ saveMetadata,
196
+ getMetadata,
197
+ deleteAllUserData,
198
+ isEnrolled,
199
+ createUserStorage,
200
+ DB_NAME,
201
+ DB_VERSION,
202
+ STORE_SHARES,
203
+ STORE_KEYS,
204
+ STORE_METADATA
205
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @file IndexedDB storage constants
3
+ */
4
+ 'use strict';
5
+
6
+ const DB_NAME = 'KeyRecoveryStore';
7
+ const DB_VERSION = 1;
8
+ const STORE_SHARES = 'shares';
9
+ const STORE_KEYS = 'keys';
10
+ const STORE_METADATA = 'metadata';
11
+
12
+ module.exports = {
13
+ DB_NAME,
14
+ DB_VERSION,
15
+ STORE_SHARES,
16
+ STORE_KEYS,
17
+ STORE_METADATA,
18
+ };
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @file IndexedDB database initialization and transaction helpers
3
+ */
4
+ 'use strict';
5
+
6
+ const {
7
+ DB_NAME,
8
+ DB_VERSION,
9
+ STORE_SHARES,
10
+ STORE_KEYS,
11
+ STORE_METADATA,
12
+ } = require('./constants');
13
+
14
+ /**
15
+ * Open the IndexedDB database
16
+ * @returns {Promise<IDBDatabase>}
17
+ */
18
+ function openDatabase() {
19
+ return new Promise((resolve, reject) => {
20
+ if (typeof indexedDB === 'undefined') {
21
+ reject(new Error('IndexedDB not available'));
22
+ return;
23
+ }
24
+
25
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
26
+
27
+ request.onerror = () => {
28
+ reject(new Error(`Failed to open database: ${request.error?.message || 'Unknown error'}`));
29
+ };
30
+
31
+ request.onsuccess = () => {
32
+ resolve(request.result);
33
+ };
34
+
35
+ request.onupgradeneeded = (event) => {
36
+ const db = event.target.result;
37
+
38
+ if (!db.objectStoreNames.contains(STORE_SHARES)) {
39
+ const sharesStore = db.createObjectStore(STORE_SHARES, { keyPath: 'id', autoIncrement: true });
40
+ sharesStore.createIndex('userId', 'userId', { unique: false });
41
+ sharesStore.createIndex('shareId', 'shareId', { unique: true });
42
+ sharesStore.createIndex('userShare', ['userId', 'shareId'], { unique: true });
43
+ }
44
+
45
+ if (!db.objectStoreNames.contains(STORE_KEYS)) {
46
+ const keysStore = db.createObjectStore(STORE_KEYS, { keyPath: 'id', autoIncrement: true });
47
+ keysStore.createIndex('userId', 'userId', { unique: false });
48
+ keysStore.createIndex('keyId', 'keyId', { unique: true });
49
+ }
50
+
51
+ if (!db.objectStoreNames.contains(STORE_METADATA)) {
52
+ db.createObjectStore(STORE_METADATA, { keyPath: 'userId' });
53
+ }
54
+ };
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Execute a transaction on the database
60
+ * @param {string|Array} storeNames - Store name(s)
61
+ * @param {string} mode - Transaction mode
62
+ * @param {Function} operation - Operation function
63
+ * @returns {Promise<*>}
64
+ */
65
+ async function withTransaction(storeNames, mode, operation) {
66
+ const db = await openDatabase();
67
+
68
+ return new Promise((resolve, reject) => {
69
+ const tx = db.transaction(storeNames, mode);
70
+
71
+ tx.onerror = () => {
72
+ reject(new Error(`Transaction failed: ${tx.error?.message || 'Unknown error'}`));
73
+ };
74
+
75
+ tx.oncomplete = () => {
76
+ db.close();
77
+ };
78
+
79
+ try {
80
+ const result = operation(tx);
81
+
82
+ if (result && typeof result.then === 'function') {
83
+ result.then(resolve).catch(reject);
84
+ } else {
85
+ tx.oncomplete = () => {
86
+ db.close();
87
+ resolve(result);
88
+ };
89
+ }
90
+ } catch (err) {
91
+ reject(err);
92
+ }
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Wrap an IDBRequest in a Promise
98
+ * @param {IDBRequest} request - Request to wrap
99
+ * @returns {Promise<*>}
100
+ */
101
+ function promisifyRequest(request) {
102
+ return new Promise((resolve, reject) => {
103
+ request.onsuccess = () => resolve(request.result);
104
+ request.onerror = () => reject(request.error);
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Clear the entire database
110
+ * @returns {Promise<void>}
111
+ */
112
+ function clearDatabase() {
113
+ return new Promise((resolve, reject) => {
114
+ if (typeof indexedDB === 'undefined') {
115
+ reject(new Error('IndexedDB not available'));
116
+ return;
117
+ }
118
+
119
+ const request = indexedDB.deleteDatabase(DB_NAME);
120
+
121
+ request.onsuccess = () => resolve();
122
+ request.onerror = () => reject(request.error);
123
+ request.onblocked = () => reject(new Error('Database deletion blocked'));
124
+ });
125
+ }
126
+
127
+ module.exports = {
128
+ openDatabase,
129
+ withTransaction,
130
+ promisifyRequest,
131
+ clearDatabase,
132
+ };
@@ -0,0 +1,27 @@
1
+ # Client Auth Storage Module
2
+
3
+ IndexedDB storage for encrypted shares and keys.
4
+
5
+ ## Directory Structure
6
+
7
+ ```
8
+ storage/
9
+ ├── constants.js - Storage constants (DB name, store names)
10
+ ├── db.js - IndexedDB connection management
11
+ ├── keys.js - Wrapped key storage operations
12
+ └── shares.js - Encrypted share storage operations
13
+ ```
14
+
15
+ ## Files
16
+
17
+ ### `constants.js`
18
+ Defines StorageError codes, database name, and object store names.
19
+
20
+ ### `db.js`
21
+ Manages IndexedDB connection lifecycle with version upgrades and cleanup.
22
+
23
+ ### `shares.js`
24
+ CRUD operations for encrypted shares in IndexedDB with versioning.
25
+
26
+ ### `keys.js`
27
+ Storage for WebAuthn-wrapped keys to decrypt S2 share.