@frontmcp/utils 0.7.2 → 0.8.1
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/crypto/hmac-signing.d.ts +96 -0
- package/crypto/index.d.ts +11 -0
- package/crypto/key-persistence/factory.d.ts +58 -0
- package/crypto/key-persistence/index.d.ts +35 -0
- package/crypto/key-persistence/key-persistence.d.ts +143 -0
- package/crypto/key-persistence/schemas.d.ts +180 -0
- package/crypto/key-persistence/types.d.ts +120 -0
- package/esm/index.mjs +1778 -583
- package/esm/package.json +17 -2
- package/fs/fs.d.ts +16 -0
- package/fs/index.d.ts +1 -1
- package/index.d.ts +3 -3
- package/index.js +1802 -585
- package/package.json +17 -2
- package/regex/safe-regex.d.ts +1 -1
- package/storage/adapters/filesystem.d.ts +144 -0
- package/storage/adapters/index.d.ts +2 -0
- package/storage/encrypted-typed-storage.d.ts +196 -0
- package/storage/encrypted-typed-storage.types.d.ts +139 -0
- package/storage/errors.d.ts +11 -0
- package/storage/index.d.ts +7 -2
- package/storage/typed-storage.d.ts +129 -0
- package/storage/typed-storage.types.d.ts +47 -0
package/esm/index.mjs
CHANGED
|
@@ -52,219 +52,8 @@ var init_jwt_alg = __esm({
|
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
// libs/utils/src/crypto/node.ts
|
|
56
|
-
var node_exports = {};
|
|
57
|
-
__export(node_exports, {
|
|
58
|
-
createSignedJwt: () => createSignedJwt,
|
|
59
|
-
generateRsaKeyPair: () => generateRsaKeyPair,
|
|
60
|
-
isRsaPssAlg: () => isRsaPssAlg,
|
|
61
|
-
jwtAlgToNodeAlg: () => jwtAlgToNodeAlg,
|
|
62
|
-
nodeCrypto: () => nodeCrypto,
|
|
63
|
-
rsaSign: () => rsaSign,
|
|
64
|
-
rsaVerify: () => rsaVerify
|
|
65
|
-
});
|
|
66
|
-
import crypto2 from "node:crypto";
|
|
67
|
-
function toUint8Array(buf) {
|
|
68
|
-
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
69
|
-
}
|
|
70
|
-
function toBuffer(data) {
|
|
71
|
-
if (typeof data === "string") {
|
|
72
|
-
return Buffer.from(data, "utf8");
|
|
73
|
-
}
|
|
74
|
-
return Buffer.from(data);
|
|
75
|
-
}
|
|
76
|
-
function generateRsaKeyPair(modulusLength = 2048, alg = "RS256") {
|
|
77
|
-
const kid = `rsa-key-${Date.now()}-${crypto2.randomBytes(8).toString("hex")}`;
|
|
78
|
-
const { privateKey, publicKey } = crypto2.generateKeyPairSync("rsa", {
|
|
79
|
-
modulusLength
|
|
80
|
-
});
|
|
81
|
-
const exported = publicKey.export({ format: "jwk" });
|
|
82
|
-
const publicJwk = {
|
|
83
|
-
...exported,
|
|
84
|
-
kid,
|
|
85
|
-
alg,
|
|
86
|
-
use: "sig",
|
|
87
|
-
kty: "RSA"
|
|
88
|
-
};
|
|
89
|
-
return { privateKey, publicKey, publicJwk };
|
|
90
|
-
}
|
|
91
|
-
function rsaSign(algorithm, data, privateKey, options) {
|
|
92
|
-
const signingKey = options ? { key: privateKey, ...options } : privateKey;
|
|
93
|
-
return crypto2.sign(algorithm, data, signingKey);
|
|
94
|
-
}
|
|
95
|
-
function rsaVerify(jwtAlg, data, publicJwk, signature) {
|
|
96
|
-
const publicKey = crypto2.createPublicKey({ key: publicJwk, format: "jwk" });
|
|
97
|
-
const nodeAlgorithm = jwtAlgToNodeAlg(jwtAlg);
|
|
98
|
-
const verifyKey = isRsaPssAlg(jwtAlg) ? {
|
|
99
|
-
key: publicKey,
|
|
100
|
-
padding: crypto2.constants.RSA_PKCS1_PSS_PADDING,
|
|
101
|
-
saltLength: crypto2.constants.RSA_PSS_SALTLEN_DIGEST
|
|
102
|
-
} : publicKey;
|
|
103
|
-
return crypto2.verify(nodeAlgorithm, data, verifyKey, signature);
|
|
104
|
-
}
|
|
105
|
-
function createSignedJwt(payload, privateKey, kid, alg = "RS256") {
|
|
106
|
-
const header = { alg, typ: "JWT", kid };
|
|
107
|
-
const headerB64 = Buffer.from(JSON.stringify(header)).toString("base64url");
|
|
108
|
-
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
109
|
-
const signatureInput = `${headerB64}.${payloadB64}`;
|
|
110
|
-
const nodeAlgorithm = jwtAlgToNodeAlg(alg);
|
|
111
|
-
const signature = rsaSign(
|
|
112
|
-
nodeAlgorithm,
|
|
113
|
-
Buffer.from(signatureInput),
|
|
114
|
-
privateKey,
|
|
115
|
-
isRsaPssAlg(alg) ? {
|
|
116
|
-
padding: crypto2.constants.RSA_PKCS1_PSS_PADDING,
|
|
117
|
-
saltLength: crypto2.constants.RSA_PSS_SALTLEN_DIGEST
|
|
118
|
-
} : void 0
|
|
119
|
-
);
|
|
120
|
-
const signatureB64 = signature.toString("base64url");
|
|
121
|
-
return `${headerB64}.${payloadB64}.${signatureB64}`;
|
|
122
|
-
}
|
|
123
|
-
var nodeCrypto;
|
|
124
|
-
var init_node = __esm({
|
|
125
|
-
"libs/utils/src/crypto/node.ts"() {
|
|
126
|
-
"use strict";
|
|
127
|
-
init_jwt_alg();
|
|
128
|
-
init_jwt_alg();
|
|
129
|
-
nodeCrypto = {
|
|
130
|
-
randomUUID() {
|
|
131
|
-
return crypto2.randomUUID();
|
|
132
|
-
},
|
|
133
|
-
randomBytes(length) {
|
|
134
|
-
return toUint8Array(crypto2.randomBytes(length));
|
|
135
|
-
},
|
|
136
|
-
sha256(data) {
|
|
137
|
-
const hash = crypto2.createHash("sha256").update(toBuffer(data)).digest();
|
|
138
|
-
return toUint8Array(hash);
|
|
139
|
-
},
|
|
140
|
-
sha256Hex(data) {
|
|
141
|
-
return crypto2.createHash("sha256").update(toBuffer(data)).digest("hex");
|
|
142
|
-
},
|
|
143
|
-
hmacSha256(key, data) {
|
|
144
|
-
const hmac2 = crypto2.createHmac("sha256", Buffer.from(key)).update(Buffer.from(data)).digest();
|
|
145
|
-
return toUint8Array(hmac2);
|
|
146
|
-
},
|
|
147
|
-
hkdfSha256(ikm, salt, info, length) {
|
|
148
|
-
const ikmBuf = Buffer.from(ikm);
|
|
149
|
-
const saltBuf = salt.length > 0 ? Buffer.from(salt) : Buffer.alloc(32);
|
|
150
|
-
const prk = crypto2.createHmac("sha256", saltBuf).update(ikmBuf).digest();
|
|
151
|
-
const hashLen = 32;
|
|
152
|
-
const n = Math.ceil(length / hashLen);
|
|
153
|
-
const chunks = [];
|
|
154
|
-
let prev = Buffer.alloc(0);
|
|
155
|
-
for (let i = 1; i <= n; i++) {
|
|
156
|
-
prev = crypto2.createHmac("sha256", prk).update(Buffer.concat([prev, Buffer.from(info), Buffer.from([i])])).digest();
|
|
157
|
-
chunks.push(prev);
|
|
158
|
-
}
|
|
159
|
-
return toUint8Array(Buffer.concat(chunks).subarray(0, length));
|
|
160
|
-
},
|
|
161
|
-
encryptAesGcm(key, plaintext, iv) {
|
|
162
|
-
const cipher = crypto2.createCipheriv("aes-256-gcm", Buffer.from(key), Buffer.from(iv));
|
|
163
|
-
const encrypted = Buffer.concat([cipher.update(Buffer.from(plaintext)), cipher.final()]);
|
|
164
|
-
const tag = cipher.getAuthTag();
|
|
165
|
-
return {
|
|
166
|
-
ciphertext: toUint8Array(encrypted),
|
|
167
|
-
tag: toUint8Array(tag)
|
|
168
|
-
};
|
|
169
|
-
},
|
|
170
|
-
decryptAesGcm(key, ciphertext, iv, tag) {
|
|
171
|
-
const decipher = crypto2.createDecipheriv("aes-256-gcm", Buffer.from(key), Buffer.from(iv));
|
|
172
|
-
decipher.setAuthTag(Buffer.from(tag));
|
|
173
|
-
const decrypted = Buffer.concat([decipher.update(Buffer.from(ciphertext)), decipher.final()]);
|
|
174
|
-
return toUint8Array(decrypted);
|
|
175
|
-
},
|
|
176
|
-
timingSafeEqual(a, b) {
|
|
177
|
-
if (a.length !== b.length) return false;
|
|
178
|
-
return crypto2.timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// libs/utils/src/crypto/browser.ts
|
|
185
|
-
var browser_exports = {};
|
|
186
|
-
__export(browser_exports, {
|
|
187
|
-
browserCrypto: () => browserCrypto
|
|
188
|
-
});
|
|
189
|
-
import { sha256 as sha256Hash } from "@noble/hashes/sha2.js";
|
|
190
|
-
import { hmac } from "@noble/hashes/hmac.js";
|
|
191
|
-
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
192
|
-
import { randomBytes as nobleRandomBytes } from "@noble/hashes/utils.js";
|
|
193
|
-
import { gcm } from "@noble/ciphers/aes.js";
|
|
194
|
-
function toBytes(data) {
|
|
195
|
-
if (typeof data === "string") {
|
|
196
|
-
return new TextEncoder().encode(data);
|
|
197
|
-
}
|
|
198
|
-
return data;
|
|
199
|
-
}
|
|
200
|
-
function toHex(bytes) {
|
|
201
|
-
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
202
|
-
}
|
|
203
|
-
function generateUUID() {
|
|
204
|
-
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
205
|
-
return crypto.randomUUID();
|
|
206
|
-
}
|
|
207
|
-
const bytes = nobleRandomBytes(16);
|
|
208
|
-
bytes[6] = bytes[6] & 15 | 64;
|
|
209
|
-
bytes[8] = bytes[8] & 63 | 128;
|
|
210
|
-
const hex = toHex(bytes);
|
|
211
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
212
|
-
}
|
|
213
|
-
function constantTimeEqual(a, b) {
|
|
214
|
-
if (a.length !== b.length) return false;
|
|
215
|
-
let result = 0;
|
|
216
|
-
for (let i = 0; i < a.length; i++) {
|
|
217
|
-
result |= a[i] ^ b[i];
|
|
218
|
-
}
|
|
219
|
-
return result === 0;
|
|
220
|
-
}
|
|
221
|
-
var browserCrypto;
|
|
222
|
-
var init_browser = __esm({
|
|
223
|
-
"libs/utils/src/crypto/browser.ts"() {
|
|
224
|
-
"use strict";
|
|
225
|
-
browserCrypto = {
|
|
226
|
-
randomUUID() {
|
|
227
|
-
return generateUUID();
|
|
228
|
-
},
|
|
229
|
-
randomBytes(length) {
|
|
230
|
-
return nobleRandomBytes(length);
|
|
231
|
-
},
|
|
232
|
-
sha256(data) {
|
|
233
|
-
return sha256Hash(toBytes(data));
|
|
234
|
-
},
|
|
235
|
-
sha256Hex(data) {
|
|
236
|
-
return toHex(sha256Hash(toBytes(data)));
|
|
237
|
-
},
|
|
238
|
-
hmacSha256(key, data) {
|
|
239
|
-
return hmac(sha256Hash, key, data);
|
|
240
|
-
},
|
|
241
|
-
hkdfSha256(ikm, salt, info, length) {
|
|
242
|
-
const effectiveSalt = salt.length > 0 ? salt : new Uint8Array(32);
|
|
243
|
-
return hkdf(sha256Hash, ikm, effectiveSalt, info, length);
|
|
244
|
-
},
|
|
245
|
-
encryptAesGcm(key, plaintext, iv) {
|
|
246
|
-
const cipher = gcm(key, iv);
|
|
247
|
-
const sealed = cipher.encrypt(plaintext);
|
|
248
|
-
const ciphertext = sealed.slice(0, -16);
|
|
249
|
-
const tag = sealed.slice(-16);
|
|
250
|
-
return { ciphertext, tag };
|
|
251
|
-
},
|
|
252
|
-
decryptAesGcm(key, ciphertext, iv, tag) {
|
|
253
|
-
const cipher = gcm(key, iv);
|
|
254
|
-
const sealed = new Uint8Array(ciphertext.length + tag.length);
|
|
255
|
-
sealed.set(ciphertext);
|
|
256
|
-
sealed.set(tag, ciphertext.length);
|
|
257
|
-
return cipher.decrypt(sealed);
|
|
258
|
-
},
|
|
259
|
-
timingSafeEqual(a, b) {
|
|
260
|
-
return constantTimeEqual(a, b);
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
|
|
266
55
|
// libs/utils/src/storage/errors.ts
|
|
267
|
-
var StorageError, StorageConnectionError, StorageOperationError, StorageNotSupportedError, StorageConfigError, StorageTTLError, StoragePatternError, StorageNotConnectedError;
|
|
56
|
+
var StorageError, StorageConnectionError, StorageOperationError, StorageNotSupportedError, StorageConfigError, StorageTTLError, StoragePatternError, StorageNotConnectedError, EncryptedStorageError;
|
|
268
57
|
var init_errors = __esm({
|
|
269
58
|
"libs/utils/src/storage/errors.ts"() {
|
|
270
59
|
"use strict";
|
|
@@ -282,7 +71,8 @@ var init_errors = __esm({
|
|
|
282
71
|
};
|
|
283
72
|
StorageConnectionError = class extends StorageError {
|
|
284
73
|
constructor(message, cause, backend) {
|
|
285
|
-
|
|
74
|
+
const fullMessage = cause ? `${message}: ${cause.message}` : message;
|
|
75
|
+
super(fullMessage, cause);
|
|
286
76
|
this.backend = backend;
|
|
287
77
|
this.name = "StorageConnectionError";
|
|
288
78
|
}
|
|
@@ -332,6 +122,12 @@ var init_errors = __esm({
|
|
|
332
122
|
this.name = "StorageNotConnectedError";
|
|
333
123
|
}
|
|
334
124
|
};
|
|
125
|
+
EncryptedStorageError = class extends StorageError {
|
|
126
|
+
constructor(message) {
|
|
127
|
+
super(message);
|
|
128
|
+
this.name = "EncryptedStorageError";
|
|
129
|
+
}
|
|
130
|
+
};
|
|
335
131
|
}
|
|
336
132
|
});
|
|
337
133
|
|
|
@@ -497,15 +293,309 @@ var init_base = __esm({
|
|
|
497
293
|
}
|
|
498
294
|
});
|
|
499
295
|
|
|
500
|
-
// libs/utils/src/storage/
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
296
|
+
// libs/utils/src/storage/utils/pattern.ts
|
|
297
|
+
function globToRegex(pattern) {
|
|
298
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
299
|
+
throw new StoragePatternError(
|
|
300
|
+
pattern.substring(0, 50) + "...",
|
|
301
|
+
`Pattern exceeds maximum length of ${MAX_PATTERN_LENGTH} characters`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const wildcardCount = (pattern.match(/[*?]/g) || []).length;
|
|
305
|
+
if (wildcardCount > MAX_WILDCARDS) {
|
|
306
|
+
throw new StoragePatternError(pattern, `Pattern has too many wildcards (max: ${MAX_WILDCARDS})`);
|
|
307
|
+
}
|
|
308
|
+
if (pattern === "" || pattern === "*") {
|
|
309
|
+
return /^.*$/;
|
|
310
|
+
}
|
|
311
|
+
let regexStr = "^";
|
|
312
|
+
let prevChar = "";
|
|
313
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
314
|
+
const char = pattern[i];
|
|
315
|
+
switch (char) {
|
|
316
|
+
case "*":
|
|
317
|
+
if (prevChar !== "*") {
|
|
318
|
+
regexStr += ".*";
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
case "?":
|
|
322
|
+
regexStr += ".";
|
|
323
|
+
break;
|
|
324
|
+
default:
|
|
325
|
+
regexStr += char.replace(REGEX_SPECIAL_CHARS, "\\$&");
|
|
326
|
+
}
|
|
327
|
+
prevChar = char;
|
|
328
|
+
}
|
|
329
|
+
regexStr += "$";
|
|
506
330
|
try {
|
|
507
|
-
return
|
|
331
|
+
return new RegExp(regexStr);
|
|
508
332
|
} catch {
|
|
333
|
+
throw new StoragePatternError(pattern, "Failed to compile pattern to regex");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function matchesPattern(key, pattern) {
|
|
337
|
+
const regex = globToRegex(pattern);
|
|
338
|
+
return regex.test(key);
|
|
339
|
+
}
|
|
340
|
+
function validatePattern(pattern) {
|
|
341
|
+
try {
|
|
342
|
+
globToRegex(pattern);
|
|
343
|
+
return { valid: true };
|
|
344
|
+
} catch (e) {
|
|
345
|
+
return {
|
|
346
|
+
valid: false,
|
|
347
|
+
error: e instanceof Error ? e.message : "Invalid pattern"
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function escapeGlob(literal) {
|
|
352
|
+
return literal.replace(/[*?\\]/g, "\\$&");
|
|
353
|
+
}
|
|
354
|
+
var MAX_PATTERN_LENGTH, MAX_WILDCARDS, REGEX_SPECIAL_CHARS;
|
|
355
|
+
var init_pattern = __esm({
|
|
356
|
+
"libs/utils/src/storage/utils/pattern.ts"() {
|
|
357
|
+
"use strict";
|
|
358
|
+
init_errors();
|
|
359
|
+
MAX_PATTERN_LENGTH = 500;
|
|
360
|
+
MAX_WILDCARDS = 20;
|
|
361
|
+
REGEX_SPECIAL_CHARS = /[.+^${}()|[\]\\]/g;
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// libs/utils/src/crypto/node.ts
|
|
366
|
+
var node_exports = {};
|
|
367
|
+
__export(node_exports, {
|
|
368
|
+
createSignedJwt: () => createSignedJwt,
|
|
369
|
+
generateRsaKeyPair: () => generateRsaKeyPair,
|
|
370
|
+
isRsaPssAlg: () => isRsaPssAlg,
|
|
371
|
+
jwtAlgToNodeAlg: () => jwtAlgToNodeAlg,
|
|
372
|
+
nodeCrypto: () => nodeCrypto,
|
|
373
|
+
rsaSign: () => rsaSign,
|
|
374
|
+
rsaVerify: () => rsaVerify
|
|
375
|
+
});
|
|
376
|
+
import crypto2 from "node:crypto";
|
|
377
|
+
function toUint8Array(buf) {
|
|
378
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
379
|
+
}
|
|
380
|
+
function toBuffer(data) {
|
|
381
|
+
if (typeof data === "string") {
|
|
382
|
+
return Buffer.from(data, "utf8");
|
|
383
|
+
}
|
|
384
|
+
return Buffer.from(data);
|
|
385
|
+
}
|
|
386
|
+
function generateRsaKeyPair(modulusLength = 2048, alg = "RS256") {
|
|
387
|
+
const kid = `rsa-key-${Date.now()}-${crypto2.randomBytes(8).toString("hex")}`;
|
|
388
|
+
const { privateKey, publicKey } = crypto2.generateKeyPairSync("rsa", {
|
|
389
|
+
modulusLength
|
|
390
|
+
});
|
|
391
|
+
const exported = publicKey.export({ format: "jwk" });
|
|
392
|
+
const publicJwk = {
|
|
393
|
+
...exported,
|
|
394
|
+
kid,
|
|
395
|
+
alg,
|
|
396
|
+
use: "sig",
|
|
397
|
+
kty: "RSA"
|
|
398
|
+
};
|
|
399
|
+
return { privateKey, publicKey, publicJwk };
|
|
400
|
+
}
|
|
401
|
+
function rsaSign(algorithm, data, privateKey, options) {
|
|
402
|
+
const signingKey = options ? { key: privateKey, ...options } : privateKey;
|
|
403
|
+
return crypto2.sign(algorithm, data, signingKey);
|
|
404
|
+
}
|
|
405
|
+
function rsaVerify(jwtAlg, data, publicJwk, signature) {
|
|
406
|
+
const publicKey = crypto2.createPublicKey({ key: publicJwk, format: "jwk" });
|
|
407
|
+
const nodeAlgorithm = jwtAlgToNodeAlg(jwtAlg);
|
|
408
|
+
const verifyKey = isRsaPssAlg(jwtAlg) ? {
|
|
409
|
+
key: publicKey,
|
|
410
|
+
padding: crypto2.constants.RSA_PKCS1_PSS_PADDING,
|
|
411
|
+
saltLength: crypto2.constants.RSA_PSS_SALTLEN_DIGEST
|
|
412
|
+
} : publicKey;
|
|
413
|
+
return crypto2.verify(nodeAlgorithm, data, verifyKey, signature);
|
|
414
|
+
}
|
|
415
|
+
function createSignedJwt(payload, privateKey, kid, alg = "RS256") {
|
|
416
|
+
const header = { alg, typ: "JWT", kid };
|
|
417
|
+
const headerB64 = Buffer.from(JSON.stringify(header)).toString("base64url");
|
|
418
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
419
|
+
const signatureInput = `${headerB64}.${payloadB64}`;
|
|
420
|
+
const nodeAlgorithm = jwtAlgToNodeAlg(alg);
|
|
421
|
+
const signature = rsaSign(
|
|
422
|
+
nodeAlgorithm,
|
|
423
|
+
Buffer.from(signatureInput),
|
|
424
|
+
privateKey,
|
|
425
|
+
isRsaPssAlg(alg) ? {
|
|
426
|
+
padding: crypto2.constants.RSA_PKCS1_PSS_PADDING,
|
|
427
|
+
saltLength: crypto2.constants.RSA_PSS_SALTLEN_DIGEST
|
|
428
|
+
} : void 0
|
|
429
|
+
);
|
|
430
|
+
const signatureB64 = signature.toString("base64url");
|
|
431
|
+
return `${headerB64}.${payloadB64}.${signatureB64}`;
|
|
432
|
+
}
|
|
433
|
+
var nodeCrypto;
|
|
434
|
+
var init_node = __esm({
|
|
435
|
+
"libs/utils/src/crypto/node.ts"() {
|
|
436
|
+
"use strict";
|
|
437
|
+
init_jwt_alg();
|
|
438
|
+
init_jwt_alg();
|
|
439
|
+
nodeCrypto = {
|
|
440
|
+
randomUUID() {
|
|
441
|
+
return crypto2.randomUUID();
|
|
442
|
+
},
|
|
443
|
+
randomBytes(length) {
|
|
444
|
+
return toUint8Array(crypto2.randomBytes(length));
|
|
445
|
+
},
|
|
446
|
+
sha256(data) {
|
|
447
|
+
const hash = crypto2.createHash("sha256").update(toBuffer(data)).digest();
|
|
448
|
+
return toUint8Array(hash);
|
|
449
|
+
},
|
|
450
|
+
sha256Hex(data) {
|
|
451
|
+
return crypto2.createHash("sha256").update(toBuffer(data)).digest("hex");
|
|
452
|
+
},
|
|
453
|
+
hmacSha256(key, data) {
|
|
454
|
+
const hmac2 = crypto2.createHmac("sha256", Buffer.from(key)).update(Buffer.from(data)).digest();
|
|
455
|
+
return toUint8Array(hmac2);
|
|
456
|
+
},
|
|
457
|
+
hkdfSha256(ikm, salt, info, length) {
|
|
458
|
+
const ikmBuf = Buffer.from(ikm);
|
|
459
|
+
const saltBuf = salt.length > 0 ? Buffer.from(salt) : Buffer.alloc(32);
|
|
460
|
+
const prk = crypto2.createHmac("sha256", saltBuf).update(ikmBuf).digest();
|
|
461
|
+
const hashLen = 32;
|
|
462
|
+
const n = Math.ceil(length / hashLen);
|
|
463
|
+
const chunks = [];
|
|
464
|
+
let prev = Buffer.alloc(0);
|
|
465
|
+
for (let i = 1; i <= n; i++) {
|
|
466
|
+
prev = crypto2.createHmac("sha256", prk).update(Buffer.concat([prev, Buffer.from(info), Buffer.from([i])])).digest();
|
|
467
|
+
chunks.push(prev);
|
|
468
|
+
}
|
|
469
|
+
return toUint8Array(Buffer.concat(chunks).subarray(0, length));
|
|
470
|
+
},
|
|
471
|
+
encryptAesGcm(key, plaintext, iv) {
|
|
472
|
+
const cipher = crypto2.createCipheriv("aes-256-gcm", Buffer.from(key), Buffer.from(iv));
|
|
473
|
+
const encrypted = Buffer.concat([cipher.update(Buffer.from(plaintext)), cipher.final()]);
|
|
474
|
+
const tag = cipher.getAuthTag();
|
|
475
|
+
return {
|
|
476
|
+
ciphertext: toUint8Array(encrypted),
|
|
477
|
+
tag: toUint8Array(tag)
|
|
478
|
+
};
|
|
479
|
+
},
|
|
480
|
+
decryptAesGcm(key, ciphertext, iv, tag) {
|
|
481
|
+
const decipher = crypto2.createDecipheriv("aes-256-gcm", Buffer.from(key), Buffer.from(iv));
|
|
482
|
+
decipher.setAuthTag(Buffer.from(tag));
|
|
483
|
+
const decrypted = Buffer.concat([decipher.update(Buffer.from(ciphertext)), decipher.final()]);
|
|
484
|
+
return toUint8Array(decrypted);
|
|
485
|
+
},
|
|
486
|
+
timingSafeEqual(a, b) {
|
|
487
|
+
if (a.length !== b.length) return false;
|
|
488
|
+
return crypto2.timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// libs/utils/src/crypto/browser.ts
|
|
495
|
+
var browser_exports = {};
|
|
496
|
+
__export(browser_exports, {
|
|
497
|
+
browserCrypto: () => browserCrypto
|
|
498
|
+
});
|
|
499
|
+
import { sha256 as sha256Hash } from "@noble/hashes/sha2.js";
|
|
500
|
+
import { hmac } from "@noble/hashes/hmac.js";
|
|
501
|
+
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
502
|
+
import { randomBytes as nobleRandomBytes } from "@noble/hashes/utils.js";
|
|
503
|
+
import { gcm } from "@noble/ciphers/aes.js";
|
|
504
|
+
function toBytes(data) {
|
|
505
|
+
if (typeof data === "string") {
|
|
506
|
+
return new TextEncoder().encode(data);
|
|
507
|
+
}
|
|
508
|
+
return data;
|
|
509
|
+
}
|
|
510
|
+
function toHex(bytes) {
|
|
511
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
512
|
+
}
|
|
513
|
+
function generateUUID() {
|
|
514
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
515
|
+
return crypto.randomUUID();
|
|
516
|
+
}
|
|
517
|
+
const bytes = nobleRandomBytes(16);
|
|
518
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
519
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
520
|
+
const hex = toHex(bytes);
|
|
521
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
522
|
+
}
|
|
523
|
+
function constantTimeEqual(a, b) {
|
|
524
|
+
if (a.length !== b.length) return false;
|
|
525
|
+
let result = 0;
|
|
526
|
+
for (let i = 0; i < a.length; i++) {
|
|
527
|
+
result |= a[i] ^ b[i];
|
|
528
|
+
}
|
|
529
|
+
return result === 0;
|
|
530
|
+
}
|
|
531
|
+
var browserCrypto;
|
|
532
|
+
var init_browser = __esm({
|
|
533
|
+
"libs/utils/src/crypto/browser.ts"() {
|
|
534
|
+
"use strict";
|
|
535
|
+
browserCrypto = {
|
|
536
|
+
randomUUID() {
|
|
537
|
+
return generateUUID();
|
|
538
|
+
},
|
|
539
|
+
randomBytes(length) {
|
|
540
|
+
return nobleRandomBytes(length);
|
|
541
|
+
},
|
|
542
|
+
sha256(data) {
|
|
543
|
+
return sha256Hash(toBytes(data));
|
|
544
|
+
},
|
|
545
|
+
sha256Hex(data) {
|
|
546
|
+
return toHex(sha256Hash(toBytes(data)));
|
|
547
|
+
},
|
|
548
|
+
hmacSha256(key, data) {
|
|
549
|
+
return hmac(sha256Hash, key, data);
|
|
550
|
+
},
|
|
551
|
+
hkdfSha256(ikm, salt, info, length) {
|
|
552
|
+
const effectiveSalt = salt.length > 0 ? salt : new Uint8Array(32);
|
|
553
|
+
return hkdf(sha256Hash, ikm, effectiveSalt, info, length);
|
|
554
|
+
},
|
|
555
|
+
encryptAesGcm(key, plaintext, iv) {
|
|
556
|
+
const cipher = gcm(key, iv);
|
|
557
|
+
const sealed = cipher.encrypt(plaintext);
|
|
558
|
+
const ciphertext = sealed.slice(0, -16);
|
|
559
|
+
const tag = sealed.slice(-16);
|
|
560
|
+
return { ciphertext, tag };
|
|
561
|
+
},
|
|
562
|
+
decryptAesGcm(key, ciphertext, iv, tag) {
|
|
563
|
+
const cipher = gcm(key, iv);
|
|
564
|
+
const sealed = new Uint8Array(ciphertext.length + tag.length);
|
|
565
|
+
sealed.set(ciphertext);
|
|
566
|
+
sealed.set(tag, ciphertext.length);
|
|
567
|
+
return cipher.decrypt(sealed);
|
|
568
|
+
},
|
|
569
|
+
timingSafeEqual(a, b) {
|
|
570
|
+
return constantTimeEqual(a, b);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// libs/utils/src/storage/utils/index.ts
|
|
577
|
+
var init_utils = __esm({
|
|
578
|
+
"libs/utils/src/storage/utils/index.ts"() {
|
|
579
|
+
"use strict";
|
|
580
|
+
init_ttl();
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// libs/utils/src/storage/adapters/redis.ts
|
|
585
|
+
var redis_exports = {};
|
|
586
|
+
__export(redis_exports, {
|
|
587
|
+
RedisStorageAdapter: () => RedisStorageAdapter
|
|
588
|
+
});
|
|
589
|
+
function getRedisClass() {
|
|
590
|
+
try {
|
|
591
|
+
return __require("ioredis").default || __require("ioredis");
|
|
592
|
+
} catch (error) {
|
|
593
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
594
|
+
if (msg.includes("Dynamic require") || msg.includes("require is not defined")) {
|
|
595
|
+
throw new Error(
|
|
596
|
+
`Failed to load ioredis: ${msg}. This typically happens with ESM bundlers (esbuild, Vite). Ensure your bundler externalizes ioredis or use CJS mode.`
|
|
597
|
+
);
|
|
598
|
+
}
|
|
509
599
|
throw new Error("ioredis is required for Redis storage adapter. Install it with: npm install ioredis");
|
|
510
600
|
}
|
|
511
601
|
}
|
|
@@ -515,7 +605,7 @@ var init_redis = __esm({
|
|
|
515
605
|
"use strict";
|
|
516
606
|
init_base();
|
|
517
607
|
init_errors();
|
|
518
|
-
|
|
608
|
+
init_utils();
|
|
519
609
|
RedisStorageAdapter = class extends BaseStorageAdapter {
|
|
520
610
|
backendName = "redis";
|
|
521
611
|
client;
|
|
@@ -1345,7 +1435,7 @@ var init_upstash = __esm({
|
|
|
1345
1435
|
});
|
|
1346
1436
|
|
|
1347
1437
|
// libs/utils/src/regex/safe-regex.ts
|
|
1348
|
-
import { analyzeForReDoS, REDOS_THRESHOLDS } from "ast
|
|
1438
|
+
import { analyzeForReDoS, REDOS_THRESHOLDS } from "@enclave-vm/ast";
|
|
1349
1439
|
var DEFAULT_MAX_INPUT_LENGTH = 5e4;
|
|
1350
1440
|
function analyzePattern(pattern, level = "polynomial") {
|
|
1351
1441
|
const patternStr = typeof pattern === "string" ? pattern : pattern.source;
|
|
@@ -1841,6 +1931,7 @@ function assertNode(feature) {
|
|
|
1841
1931
|
|
|
1842
1932
|
// libs/utils/src/fs/fs.ts
|
|
1843
1933
|
var _fsp = null;
|
|
1934
|
+
var _fs = null;
|
|
1844
1935
|
var _spawn = null;
|
|
1845
1936
|
function getFsp() {
|
|
1846
1937
|
if (!_fsp) {
|
|
@@ -1849,8 +1940,15 @@ function getFsp() {
|
|
|
1849
1940
|
}
|
|
1850
1941
|
return _fsp;
|
|
1851
1942
|
}
|
|
1852
|
-
function
|
|
1853
|
-
if (!
|
|
1943
|
+
function getFs() {
|
|
1944
|
+
if (!_fs) {
|
|
1945
|
+
assertNode("File system operations");
|
|
1946
|
+
_fs = __require("fs");
|
|
1947
|
+
}
|
|
1948
|
+
return _fs;
|
|
1949
|
+
}
|
|
1950
|
+
function getSpawn() {
|
|
1951
|
+
if (!_spawn) {
|
|
1854
1952
|
assertNode("Child process operations");
|
|
1855
1953
|
_spawn = __require("child_process").spawn;
|
|
1856
1954
|
}
|
|
@@ -1861,6 +1959,10 @@ async function readFile(p, encoding = "utf8") {
|
|
|
1861
1959
|
const fsp = getFsp();
|
|
1862
1960
|
return fsp.readFile(p, encoding);
|
|
1863
1961
|
}
|
|
1962
|
+
function readFileSync(p, encoding = "utf8") {
|
|
1963
|
+
const fs = getFs();
|
|
1964
|
+
return fs.readFileSync(p, encoding);
|
|
1965
|
+
}
|
|
1864
1966
|
async function readFileBuffer(p) {
|
|
1865
1967
|
const fsp = getFsp();
|
|
1866
1968
|
return fsp.readFile(p);
|
|
@@ -2356,379 +2458,372 @@ function isSecretCached(options) {
|
|
|
2356
2458
|
return secretCache.has(cacheKey);
|
|
2357
2459
|
}
|
|
2358
2460
|
|
|
2359
|
-
// libs/utils/src/crypto/
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
return getCrypto().randomUUID();
|
|
2461
|
+
// libs/utils/src/crypto/hmac-signing.ts
|
|
2462
|
+
function computeSignature(data, secret) {
|
|
2463
|
+
const encoder = new TextEncoder();
|
|
2464
|
+
const keyBytes = encoder.encode(secret);
|
|
2465
|
+
const dataBytes = encoder.encode(data);
|
|
2466
|
+
const hmac2 = hmacSha256(keyBytes, dataBytes);
|
|
2467
|
+
return base64urlEncode(hmac2);
|
|
2468
|
+
}
|
|
2469
|
+
function signData(data, config) {
|
|
2470
|
+
const jsonData = JSON.stringify(data);
|
|
2471
|
+
const sig = computeSignature(jsonData, config.secret);
|
|
2472
|
+
const signed = {
|
|
2473
|
+
data,
|
|
2474
|
+
sig,
|
|
2475
|
+
v: 1
|
|
2476
|
+
};
|
|
2477
|
+
return JSON.stringify(signed);
|
|
2377
2478
|
}
|
|
2378
|
-
function
|
|
2379
|
-
|
|
2380
|
-
|
|
2479
|
+
function verifyData(signedJson, config) {
|
|
2480
|
+
try {
|
|
2481
|
+
const parsed = JSON.parse(signedJson);
|
|
2482
|
+
if (!parsed || typeof parsed !== "object" || !("sig" in parsed)) {
|
|
2483
|
+
return null;
|
|
2484
|
+
}
|
|
2485
|
+
const signed = parsed;
|
|
2486
|
+
if (signed.v !== 1) {
|
|
2487
|
+
return null;
|
|
2488
|
+
}
|
|
2489
|
+
const jsonData = JSON.stringify(signed.data);
|
|
2490
|
+
const expectedSig = computeSignature(jsonData, config.secret);
|
|
2491
|
+
const sigBytes = base64urlDecode(signed.sig);
|
|
2492
|
+
const expectedBytes = base64urlDecode(expectedSig);
|
|
2493
|
+
if (sigBytes.length !== expectedBytes.length) {
|
|
2494
|
+
return null;
|
|
2495
|
+
}
|
|
2496
|
+
if (!timingSafeEqual(sigBytes, expectedBytes)) {
|
|
2497
|
+
return null;
|
|
2498
|
+
}
|
|
2499
|
+
return signed.data;
|
|
2500
|
+
} catch {
|
|
2501
|
+
return null;
|
|
2381
2502
|
}
|
|
2382
|
-
return getCrypto().randomBytes(length);
|
|
2383
|
-
}
|
|
2384
|
-
function sha256(data) {
|
|
2385
|
-
return getCrypto().sha256(data);
|
|
2386
|
-
}
|
|
2387
|
-
function sha256Hex(data) {
|
|
2388
|
-
return getCrypto().sha256Hex(data);
|
|
2389
|
-
}
|
|
2390
|
-
function hmacSha256(key, data) {
|
|
2391
|
-
return getCrypto().hmacSha256(key, data);
|
|
2392
2503
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
throw new Error(`HKDF-SHA256 length cannot exceed ${HKDF_SHA256_MAX_LENGTH} bytes, got ${length}`);
|
|
2504
|
+
function isSignedData(json) {
|
|
2505
|
+
try {
|
|
2506
|
+
const parsed = JSON.parse(json);
|
|
2507
|
+
return parsed && typeof parsed === "object" && "sig" in parsed && "v" in parsed;
|
|
2508
|
+
} catch {
|
|
2509
|
+
return false;
|
|
2400
2510
|
}
|
|
2401
|
-
return getCrypto().hkdfSha256(ikm, salt, info, length);
|
|
2402
2511
|
}
|
|
2403
|
-
function
|
|
2404
|
-
if (
|
|
2405
|
-
|
|
2512
|
+
function verifyOrParseData(json, config) {
|
|
2513
|
+
if (isSignedData(json)) {
|
|
2514
|
+
return verifyData(json, config);
|
|
2406
2515
|
}
|
|
2407
|
-
|
|
2408
|
-
|
|
2516
|
+
try {
|
|
2517
|
+
return JSON.parse(json);
|
|
2518
|
+
} catch {
|
|
2519
|
+
return null;
|
|
2409
2520
|
}
|
|
2410
|
-
return getCrypto().encryptAesGcm(key, plaintext, iv);
|
|
2411
2521
|
}
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2522
|
+
|
|
2523
|
+
// libs/utils/src/crypto/key-persistence/schemas.ts
|
|
2524
|
+
import { z as z2 } from "zod";
|
|
2525
|
+
var MAX_CLOCK_DRIFT_MS2 = 6e4;
|
|
2526
|
+
var MAX_KEY_AGE_MS = 100 * 365 * 24 * 60 * 60 * 1e3;
|
|
2527
|
+
var asymmetricAlgSchema = z2.enum(["RS256", "RS384", "RS512", "ES256", "ES384", "ES512"]);
|
|
2528
|
+
var baseKeyDataSchema = z2.object({
|
|
2529
|
+
kid: z2.string().min(1),
|
|
2530
|
+
createdAt: z2.number().positive().int(),
|
|
2531
|
+
version: z2.number().positive().int()
|
|
2532
|
+
});
|
|
2533
|
+
var jsonWebKeySchema = z2.object({
|
|
2534
|
+
kty: z2.string().optional(),
|
|
2535
|
+
use: z2.string().optional(),
|
|
2536
|
+
alg: z2.string().optional(),
|
|
2537
|
+
kid: z2.string().optional(),
|
|
2538
|
+
n: z2.string().optional(),
|
|
2539
|
+
e: z2.string().optional(),
|
|
2540
|
+
d: z2.string().optional(),
|
|
2541
|
+
p: z2.string().optional(),
|
|
2542
|
+
q: z2.string().optional(),
|
|
2543
|
+
dp: z2.string().optional(),
|
|
2544
|
+
dq: z2.string().optional(),
|
|
2545
|
+
qi: z2.string().optional(),
|
|
2546
|
+
x: z2.string().optional(),
|
|
2547
|
+
y: z2.string().optional(),
|
|
2548
|
+
crv: z2.string().optional()
|
|
2549
|
+
}).passthrough();
|
|
2550
|
+
var secretKeyDataSchema = baseKeyDataSchema.extend({
|
|
2551
|
+
type: z2.literal("secret"),
|
|
2552
|
+
secret: z2.string().min(1),
|
|
2553
|
+
bytes: z2.number().positive().int()
|
|
2554
|
+
});
|
|
2555
|
+
var asymmetricKeyDataSchema = baseKeyDataSchema.extend({
|
|
2556
|
+
type: z2.literal("asymmetric"),
|
|
2557
|
+
alg: asymmetricAlgSchema,
|
|
2558
|
+
privateKey: jsonWebKeySchema,
|
|
2559
|
+
publicJwk: z2.object({
|
|
2560
|
+
keys: z2.array(jsonWebKeySchema).min(1)
|
|
2561
|
+
})
|
|
2562
|
+
});
|
|
2563
|
+
var anyKeyDataSchema = z2.discriminatedUnion("type", [secretKeyDataSchema, asymmetricKeyDataSchema]);
|
|
2564
|
+
function validateKeyData(data) {
|
|
2565
|
+
const result = anyKeyDataSchema.safeParse(data);
|
|
2566
|
+
if (!result.success) {
|
|
2567
|
+
return {
|
|
2568
|
+
valid: false,
|
|
2569
|
+
error: result.error.issues[0]?.message ?? "Invalid key structure"
|
|
2570
|
+
};
|
|
2415
2571
|
}
|
|
2416
|
-
|
|
2417
|
-
|
|
2572
|
+
const parsed = result.data;
|
|
2573
|
+
const now = Date.now();
|
|
2574
|
+
if (parsed.createdAt > now + MAX_CLOCK_DRIFT_MS2) {
|
|
2575
|
+
return { valid: false, error: "createdAt is in the future" };
|
|
2418
2576
|
}
|
|
2419
|
-
if (
|
|
2420
|
-
|
|
2577
|
+
if (parsed.createdAt < now - MAX_KEY_AGE_MS) {
|
|
2578
|
+
return { valid: false, error: "createdAt is too old" };
|
|
2421
2579
|
}
|
|
2422
|
-
return
|
|
2580
|
+
return { valid: true, data: parsed };
|
|
2423
2581
|
}
|
|
2424
|
-
function
|
|
2425
|
-
|
|
2426
|
-
|
|
2582
|
+
function parseKeyData(data) {
|
|
2583
|
+
const validation = validateKeyData(data);
|
|
2584
|
+
if (!validation.valid) {
|
|
2585
|
+
return null;
|
|
2427
2586
|
}
|
|
2428
|
-
return
|
|
2587
|
+
return validation.data;
|
|
2429
2588
|
}
|
|
2430
|
-
function
|
|
2431
|
-
return
|
|
2589
|
+
function isSecretKeyData(data) {
|
|
2590
|
+
return data.type === "secret";
|
|
2432
2591
|
}
|
|
2433
|
-
function
|
|
2434
|
-
|
|
2435
|
-
if (typeof Buffer !== "undefined") {
|
|
2436
|
-
base64 = Buffer.from(data).toString("base64");
|
|
2437
|
-
} else {
|
|
2438
|
-
const binString = Array.from(data, (byte) => String.fromCodePoint(byte)).join("");
|
|
2439
|
-
base64 = btoa(binString);
|
|
2440
|
-
}
|
|
2441
|
-
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2592
|
+
function isAsymmetricKeyData(data) {
|
|
2593
|
+
return data.type === "asymmetric";
|
|
2442
2594
|
}
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2595
|
+
|
|
2596
|
+
// libs/utils/src/crypto/key-persistence/key-persistence.ts
|
|
2597
|
+
var KeyPersistence = class {
|
|
2598
|
+
storage;
|
|
2599
|
+
cache = /* @__PURE__ */ new Map();
|
|
2600
|
+
enableCache;
|
|
2601
|
+
throwOnInvalid;
|
|
2602
|
+
constructor(options) {
|
|
2603
|
+
this.storage = options.storage;
|
|
2604
|
+
this.enableCache = options.enableCache ?? true;
|
|
2605
|
+
this.throwOnInvalid = options.throwOnInvalid ?? false;
|
|
2448
2606
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2607
|
+
/**
|
|
2608
|
+
* Get a key by ID.
|
|
2609
|
+
*
|
|
2610
|
+
* @param kid - Key identifier
|
|
2611
|
+
* @returns Key data or null if not found
|
|
2612
|
+
*/
|
|
2613
|
+
async get(kid) {
|
|
2614
|
+
if (this.enableCache) {
|
|
2615
|
+
const cached = this.cache.get(kid);
|
|
2616
|
+
if (cached) return cached;
|
|
2617
|
+
}
|
|
2618
|
+
const raw = await this.storage.get(kid);
|
|
2619
|
+
if (!raw) return null;
|
|
2620
|
+
let parsed;
|
|
2621
|
+
try {
|
|
2622
|
+
parsed = JSON.parse(raw);
|
|
2623
|
+
} catch {
|
|
2624
|
+
if (this.throwOnInvalid) {
|
|
2625
|
+
throw new Error(`Invalid JSON for key "${kid}"`);
|
|
2626
|
+
}
|
|
2627
|
+
return null;
|
|
2628
|
+
}
|
|
2629
|
+
const validation = validateKeyData(parsed);
|
|
2630
|
+
if (!validation.valid) {
|
|
2631
|
+
if (this.throwOnInvalid) {
|
|
2632
|
+
throw new Error(`Invalid key data for "${kid}": ${validation.error}`);
|
|
2633
|
+
}
|
|
2634
|
+
return null;
|
|
2635
|
+
}
|
|
2636
|
+
const key = validation.data;
|
|
2637
|
+
if (this.enableCache) {
|
|
2638
|
+
this.cache.set(kid, key);
|
|
2639
|
+
}
|
|
2640
|
+
return key;
|
|
2454
2641
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
function buildPrefix(name, id) {
|
|
2466
|
-
if (id) {
|
|
2467
|
-
return `${name}${NAMESPACE_SEPARATOR}${id}${NAMESPACE_SEPARATOR}`;
|
|
2642
|
+
/**
|
|
2643
|
+
* Get a secret key by ID.
|
|
2644
|
+
*
|
|
2645
|
+
* @param kid - Key identifier
|
|
2646
|
+
* @returns Secret key data or null if not found or wrong type
|
|
2647
|
+
*/
|
|
2648
|
+
async getSecret(kid) {
|
|
2649
|
+
const key = await this.get(kid);
|
|
2650
|
+
if (!key || !isSecretKeyData(key)) return null;
|
|
2651
|
+
return key;
|
|
2468
2652
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2653
|
+
/**
|
|
2654
|
+
* Get an asymmetric key by ID.
|
|
2655
|
+
*
|
|
2656
|
+
* @param kid - Key identifier
|
|
2657
|
+
* @returns Asymmetric key data or null if not found or wrong type
|
|
2658
|
+
*/
|
|
2659
|
+
async getAsymmetric(kid) {
|
|
2660
|
+
const key = await this.get(kid);
|
|
2661
|
+
if (!key || !isAsymmetricKeyData(key)) return null;
|
|
2662
|
+
return key;
|
|
2476
2663
|
}
|
|
2477
|
-
// ============================================
|
|
2478
|
-
// Key Prefixing Helpers
|
|
2479
|
-
// ============================================
|
|
2480
2664
|
/**
|
|
2481
|
-
*
|
|
2665
|
+
* Store a key.
|
|
2666
|
+
*
|
|
2667
|
+
* @param key - Key data to store
|
|
2482
2668
|
*/
|
|
2483
|
-
|
|
2484
|
-
|
|
2669
|
+
async set(key) {
|
|
2670
|
+
const validation = validateKeyData(key);
|
|
2671
|
+
if (!validation.valid) {
|
|
2672
|
+
throw new Error(`Invalid key data: ${validation.error}`);
|
|
2673
|
+
}
|
|
2674
|
+
await this.storage.set(key.kid, JSON.stringify(key, null, 2));
|
|
2675
|
+
if (this.enableCache) {
|
|
2676
|
+
this.cache.set(key.kid, key);
|
|
2677
|
+
}
|
|
2485
2678
|
}
|
|
2486
2679
|
/**
|
|
2487
|
-
*
|
|
2680
|
+
* Delete a key.
|
|
2681
|
+
*
|
|
2682
|
+
* @param kid - Key identifier
|
|
2683
|
+
* @returns true if key existed and was deleted
|
|
2488
2684
|
*/
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2685
|
+
async delete(kid) {
|
|
2686
|
+
this.cache.delete(kid);
|
|
2687
|
+
return this.storage.delete(kid);
|
|
2688
|
+
}
|
|
2689
|
+
/**
|
|
2690
|
+
* Check if a key exists.
|
|
2691
|
+
*
|
|
2692
|
+
* @param kid - Key identifier
|
|
2693
|
+
* @returns true if key exists
|
|
2694
|
+
*/
|
|
2695
|
+
async has(kid) {
|
|
2696
|
+
if (this.enableCache && this.cache.has(kid)) {
|
|
2697
|
+
return true;
|
|
2492
2698
|
}
|
|
2493
|
-
return
|
|
2699
|
+
return this.storage.exists(kid);
|
|
2494
2700
|
}
|
|
2495
2701
|
/**
|
|
2496
|
-
*
|
|
2702
|
+
* List all key IDs.
|
|
2703
|
+
*
|
|
2704
|
+
* @returns Array of key identifiers
|
|
2497
2705
|
*/
|
|
2498
|
-
|
|
2499
|
-
return this.
|
|
2706
|
+
async list() {
|
|
2707
|
+
return this.storage.keys();
|
|
2500
2708
|
}
|
|
2501
2709
|
/**
|
|
2502
|
-
*
|
|
2710
|
+
* Get or create a secret key.
|
|
2711
|
+
*
|
|
2712
|
+
* If a key with the given ID exists and is a valid secret key,
|
|
2713
|
+
* it is returned. Otherwise, a new secret key is generated.
|
|
2714
|
+
*
|
|
2715
|
+
* @param kid - Key identifier
|
|
2716
|
+
* @param options - Options for key creation
|
|
2717
|
+
* @returns Secret key data
|
|
2718
|
+
*
|
|
2719
|
+
* @example
|
|
2720
|
+
* ```typescript
|
|
2721
|
+
* const secret = await keys.getOrCreateSecret('encryption-key');
|
|
2722
|
+
* const bytes = base64urlDecode(secret.secret);
|
|
2723
|
+
* ```
|
|
2503
2724
|
*/
|
|
2504
|
-
|
|
2505
|
-
|
|
2725
|
+
async getOrCreateSecret(kid, options) {
|
|
2726
|
+
const existing = await this.getSecret(kid);
|
|
2727
|
+
if (existing) {
|
|
2728
|
+
return existing;
|
|
2729
|
+
}
|
|
2730
|
+
const bytes = options?.bytes ?? 32;
|
|
2731
|
+
const secret = {
|
|
2732
|
+
type: "secret",
|
|
2733
|
+
kid,
|
|
2734
|
+
secret: base64urlEncode(randomBytes(bytes)),
|
|
2735
|
+
bytes,
|
|
2736
|
+
createdAt: Date.now(),
|
|
2737
|
+
version: 1
|
|
2738
|
+
};
|
|
2739
|
+
await this.set(secret);
|
|
2740
|
+
return secret;
|
|
2506
2741
|
}
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2742
|
+
/**
|
|
2743
|
+
* Clear the in-memory cache.
|
|
2744
|
+
*
|
|
2745
|
+
* Useful when you want to force a reload from storage.
|
|
2746
|
+
*/
|
|
2747
|
+
clearCache() {
|
|
2748
|
+
this.cache.clear();
|
|
2749
|
+
}
|
|
2750
|
+
/**
|
|
2751
|
+
* Clear cache for a specific key.
|
|
2752
|
+
*
|
|
2753
|
+
* @param kid - Key identifier
|
|
2754
|
+
*/
|
|
2755
|
+
clearCacheFor(kid) {
|
|
2756
|
+
this.cache.delete(kid);
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Check if a key is in the cache.
|
|
2760
|
+
*
|
|
2761
|
+
* @param kid - Key identifier
|
|
2762
|
+
* @returns true if key is cached
|
|
2763
|
+
*/
|
|
2764
|
+
isCached(kid) {
|
|
2765
|
+
return this.cache.has(kid);
|
|
2766
|
+
}
|
|
2767
|
+
/**
|
|
2768
|
+
* Get the underlying storage adapter.
|
|
2769
|
+
*
|
|
2770
|
+
* Use with caution - direct storage access bypasses validation.
|
|
2771
|
+
*/
|
|
2772
|
+
getAdapter() {
|
|
2773
|
+
return this.storage;
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
|
|
2777
|
+
// libs/utils/src/storage/adapters/memory.ts
|
|
2778
|
+
init_base();
|
|
2779
|
+
init_errors();
|
|
2780
|
+
init_pattern();
|
|
2781
|
+
init_ttl();
|
|
2782
|
+
import { EventEmitter } from "events";
|
|
2783
|
+
var DEFAULT_SWEEP_INTERVAL_SECONDS = 60;
|
|
2784
|
+
var MAX_TIMEOUT_MS = 2147483647;
|
|
2785
|
+
var MemoryStorageAdapter = class extends BaseStorageAdapter {
|
|
2786
|
+
backendName = "memory";
|
|
2787
|
+
store = /* @__PURE__ */ new Map();
|
|
2788
|
+
emitter = new EventEmitter();
|
|
2789
|
+
sweepInterval;
|
|
2790
|
+
options;
|
|
2791
|
+
// LRU tracking (simple linked list via insertion order in Map)
|
|
2792
|
+
accessOrder = [];
|
|
2793
|
+
constructor(options = {}) {
|
|
2794
|
+
super();
|
|
2795
|
+
this.options = {
|
|
2796
|
+
enableSweeper: options.enableSweeper ?? true,
|
|
2797
|
+
sweepIntervalSeconds: options.sweepIntervalSeconds ?? DEFAULT_SWEEP_INTERVAL_SECONDS,
|
|
2798
|
+
maxEntries: options.maxEntries ?? 0
|
|
2799
|
+
// 0 = unlimited
|
|
2800
|
+
};
|
|
2801
|
+
this.emitter.setMaxListeners(1e3);
|
|
2513
2802
|
}
|
|
2514
2803
|
// ============================================
|
|
2515
|
-
// Connection Lifecycle
|
|
2804
|
+
// Connection Lifecycle
|
|
2516
2805
|
// ============================================
|
|
2517
2806
|
async connect() {
|
|
2518
|
-
|
|
2807
|
+
if (this.connected) return;
|
|
2808
|
+
this.connected = true;
|
|
2809
|
+
if (this.options.enableSweeper && this.options.sweepIntervalSeconds > 0) {
|
|
2810
|
+
this.startSweeper();
|
|
2811
|
+
}
|
|
2519
2812
|
}
|
|
2520
2813
|
async disconnect() {
|
|
2521
|
-
|
|
2814
|
+
if (!this.connected) return;
|
|
2815
|
+
this.stopSweeper();
|
|
2816
|
+
this.clearAllTimeouts();
|
|
2817
|
+
this.store.clear();
|
|
2818
|
+
this.accessOrder = [];
|
|
2819
|
+
this.emitter.removeAllListeners();
|
|
2820
|
+
this.connected = false;
|
|
2522
2821
|
}
|
|
2523
2822
|
async ping() {
|
|
2524
|
-
return this.
|
|
2823
|
+
return this.connected;
|
|
2525
2824
|
}
|
|
2526
2825
|
// ============================================
|
|
2527
|
-
// Core Operations
|
|
2528
|
-
// ============================================
|
|
2529
|
-
async get(key) {
|
|
2530
|
-
return this.adapter.get(this.prefixKey(key));
|
|
2531
|
-
}
|
|
2532
|
-
async set(key, value, options) {
|
|
2533
|
-
return this.adapter.set(this.prefixKey(key), value, options);
|
|
2534
|
-
}
|
|
2535
|
-
async delete(key) {
|
|
2536
|
-
return this.adapter.delete(this.prefixKey(key));
|
|
2537
|
-
}
|
|
2538
|
-
async exists(key) {
|
|
2539
|
-
return this.adapter.exists(this.prefixKey(key));
|
|
2540
|
-
}
|
|
2541
|
-
// ============================================
|
|
2542
|
-
// Batch Operations (with prefixing)
|
|
2543
|
-
// ============================================
|
|
2544
|
-
async mget(keys) {
|
|
2545
|
-
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
2546
|
-
return this.adapter.mget(prefixedKeys);
|
|
2547
|
-
}
|
|
2548
|
-
async mset(entries) {
|
|
2549
|
-
const prefixedEntries = entries.map((e) => ({
|
|
2550
|
-
...e,
|
|
2551
|
-
key: this.prefixKey(e.key)
|
|
2552
|
-
}));
|
|
2553
|
-
return this.adapter.mset(prefixedEntries);
|
|
2554
|
-
}
|
|
2555
|
-
async mdelete(keys) {
|
|
2556
|
-
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
2557
|
-
return this.adapter.mdelete(prefixedKeys);
|
|
2558
|
-
}
|
|
2559
|
-
// ============================================
|
|
2560
|
-
// TTL Operations (with prefixing)
|
|
2561
|
-
// ============================================
|
|
2562
|
-
async expire(key, ttlSeconds) {
|
|
2563
|
-
return this.adapter.expire(this.prefixKey(key), ttlSeconds);
|
|
2564
|
-
}
|
|
2565
|
-
async ttl(key) {
|
|
2566
|
-
return this.adapter.ttl(this.prefixKey(key));
|
|
2567
|
-
}
|
|
2568
|
-
// ============================================
|
|
2569
|
-
// Key Enumeration (with prefixing)
|
|
2570
|
-
// ============================================
|
|
2571
|
-
async keys(pattern = "*") {
|
|
2572
|
-
const prefixedPattern = this.prefixPattern(pattern);
|
|
2573
|
-
const keys = await this.adapter.keys(prefixedPattern);
|
|
2574
|
-
return keys.map((k) => this.unprefixKey(k));
|
|
2575
|
-
}
|
|
2576
|
-
async count(pattern = "*") {
|
|
2577
|
-
const prefixedPattern = this.prefixPattern(pattern);
|
|
2578
|
-
return this.adapter.count(prefixedPattern);
|
|
2579
|
-
}
|
|
2580
|
-
// ============================================
|
|
2581
|
-
// Atomic Operations (with prefixing)
|
|
2582
|
-
// ============================================
|
|
2583
|
-
async incr(key) {
|
|
2584
|
-
return this.adapter.incr(this.prefixKey(key));
|
|
2585
|
-
}
|
|
2586
|
-
async decr(key) {
|
|
2587
|
-
return this.adapter.decr(this.prefixKey(key));
|
|
2588
|
-
}
|
|
2589
|
-
async incrBy(key, amount) {
|
|
2590
|
-
return this.adapter.incrBy(this.prefixKey(key), amount);
|
|
2591
|
-
}
|
|
2592
|
-
// ============================================
|
|
2593
|
-
// Pub/Sub (with channel prefixing)
|
|
2594
|
-
// ============================================
|
|
2595
|
-
async publish(channel, message) {
|
|
2596
|
-
return this.adapter.publish(this.prefixChannel(channel), message);
|
|
2597
|
-
}
|
|
2598
|
-
async subscribe(channel, handler) {
|
|
2599
|
-
const prefixedChannel = this.prefixChannel(channel);
|
|
2600
|
-
const wrappedHandler = (message, ch) => {
|
|
2601
|
-
const unprefixedChannel = ch.startsWith(this.prefix) ? ch.slice(this.prefix.length) : ch;
|
|
2602
|
-
handler(message, unprefixedChannel);
|
|
2603
|
-
};
|
|
2604
|
-
return this.adapter.subscribe(prefixedChannel, wrappedHandler);
|
|
2605
|
-
}
|
|
2606
|
-
supportsPubSub() {
|
|
2607
|
-
return this.adapter.supportsPubSub();
|
|
2608
|
-
}
|
|
2609
|
-
};
|
|
2610
|
-
function createRootStorage(adapter) {
|
|
2611
|
-
return new NamespacedStorageImpl(adapter, "", adapter);
|
|
2612
|
-
}
|
|
2613
|
-
function createNamespacedStorage(adapter, prefix) {
|
|
2614
|
-
const normalizedPrefix = prefix && !prefix.endsWith(NAMESPACE_SEPARATOR) ? prefix + NAMESPACE_SEPARATOR : prefix;
|
|
2615
|
-
return new NamespacedStorageImpl(adapter, normalizedPrefix, adapter);
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
|
-
// libs/utils/src/storage/adapters/memory.ts
|
|
2619
|
-
init_base();
|
|
2620
|
-
init_errors();
|
|
2621
|
-
import { EventEmitter } from "events";
|
|
2622
|
-
|
|
2623
|
-
// libs/utils/src/storage/utils/pattern.ts
|
|
2624
|
-
init_errors();
|
|
2625
|
-
var MAX_PATTERN_LENGTH = 500;
|
|
2626
|
-
var MAX_WILDCARDS = 20;
|
|
2627
|
-
var REGEX_SPECIAL_CHARS = /[.+^${}()|[\]\\]/g;
|
|
2628
|
-
function globToRegex(pattern) {
|
|
2629
|
-
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
2630
|
-
throw new StoragePatternError(
|
|
2631
|
-
pattern.substring(0, 50) + "...",
|
|
2632
|
-
`Pattern exceeds maximum length of ${MAX_PATTERN_LENGTH} characters`
|
|
2633
|
-
);
|
|
2634
|
-
}
|
|
2635
|
-
const wildcardCount = (pattern.match(/[*?]/g) || []).length;
|
|
2636
|
-
if (wildcardCount > MAX_WILDCARDS) {
|
|
2637
|
-
throw new StoragePatternError(pattern, `Pattern has too many wildcards (max: ${MAX_WILDCARDS})`);
|
|
2638
|
-
}
|
|
2639
|
-
if (pattern === "" || pattern === "*") {
|
|
2640
|
-
return /^.*$/;
|
|
2641
|
-
}
|
|
2642
|
-
let regexStr = "^";
|
|
2643
|
-
let prevChar = "";
|
|
2644
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
2645
|
-
const char = pattern[i];
|
|
2646
|
-
switch (char) {
|
|
2647
|
-
case "*":
|
|
2648
|
-
if (prevChar !== "*") {
|
|
2649
|
-
regexStr += ".*";
|
|
2650
|
-
}
|
|
2651
|
-
break;
|
|
2652
|
-
case "?":
|
|
2653
|
-
regexStr += ".";
|
|
2654
|
-
break;
|
|
2655
|
-
default:
|
|
2656
|
-
regexStr += char.replace(REGEX_SPECIAL_CHARS, "\\$&");
|
|
2657
|
-
}
|
|
2658
|
-
prevChar = char;
|
|
2659
|
-
}
|
|
2660
|
-
regexStr += "$";
|
|
2661
|
-
try {
|
|
2662
|
-
return new RegExp(regexStr);
|
|
2663
|
-
} catch {
|
|
2664
|
-
throw new StoragePatternError(pattern, "Failed to compile pattern to regex");
|
|
2665
|
-
}
|
|
2666
|
-
}
|
|
2667
|
-
function matchesPattern(key, pattern) {
|
|
2668
|
-
const regex = globToRegex(pattern);
|
|
2669
|
-
return regex.test(key);
|
|
2670
|
-
}
|
|
2671
|
-
function validatePattern(pattern) {
|
|
2672
|
-
try {
|
|
2673
|
-
globToRegex(pattern);
|
|
2674
|
-
return { valid: true };
|
|
2675
|
-
} catch (e) {
|
|
2676
|
-
return {
|
|
2677
|
-
valid: false,
|
|
2678
|
-
error: e instanceof Error ? e.message : "Invalid pattern"
|
|
2679
|
-
};
|
|
2680
|
-
}
|
|
2681
|
-
}
|
|
2682
|
-
function escapeGlob(literal) {
|
|
2683
|
-
return literal.replace(/[*?\\]/g, "\\$&");
|
|
2684
|
-
}
|
|
2685
|
-
|
|
2686
|
-
// libs/utils/src/storage/adapters/memory.ts
|
|
2687
|
-
init_ttl();
|
|
2688
|
-
var DEFAULT_SWEEP_INTERVAL_SECONDS = 60;
|
|
2689
|
-
var MAX_TIMEOUT_MS = 2147483647;
|
|
2690
|
-
var MemoryStorageAdapter = class extends BaseStorageAdapter {
|
|
2691
|
-
backendName = "memory";
|
|
2692
|
-
store = /* @__PURE__ */ new Map();
|
|
2693
|
-
emitter = new EventEmitter();
|
|
2694
|
-
sweepInterval;
|
|
2695
|
-
options;
|
|
2696
|
-
// LRU tracking (simple linked list via insertion order in Map)
|
|
2697
|
-
accessOrder = [];
|
|
2698
|
-
constructor(options = {}) {
|
|
2699
|
-
super();
|
|
2700
|
-
this.options = {
|
|
2701
|
-
enableSweeper: options.enableSweeper ?? true,
|
|
2702
|
-
sweepIntervalSeconds: options.sweepIntervalSeconds ?? DEFAULT_SWEEP_INTERVAL_SECONDS,
|
|
2703
|
-
maxEntries: options.maxEntries ?? 0
|
|
2704
|
-
// 0 = unlimited
|
|
2705
|
-
};
|
|
2706
|
-
this.emitter.setMaxListeners(1e3);
|
|
2707
|
-
}
|
|
2708
|
-
// ============================================
|
|
2709
|
-
// Connection Lifecycle
|
|
2710
|
-
// ============================================
|
|
2711
|
-
async connect() {
|
|
2712
|
-
if (this.connected) return;
|
|
2713
|
-
this.connected = true;
|
|
2714
|
-
if (this.options.enableSweeper && this.options.sweepIntervalSeconds > 0) {
|
|
2715
|
-
this.startSweeper();
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2718
|
-
async disconnect() {
|
|
2719
|
-
if (!this.connected) return;
|
|
2720
|
-
this.stopSweeper();
|
|
2721
|
-
this.clearAllTimeouts();
|
|
2722
|
-
this.store.clear();
|
|
2723
|
-
this.accessOrder = [];
|
|
2724
|
-
this.emitter.removeAllListeners();
|
|
2725
|
-
this.connected = false;
|
|
2726
|
-
}
|
|
2727
|
-
async ping() {
|
|
2728
|
-
return this.connected;
|
|
2729
|
-
}
|
|
2730
|
-
// ============================================
|
|
2731
|
-
// Core Operations
|
|
2826
|
+
// Core Operations
|
|
2732
2827
|
// ============================================
|
|
2733
2828
|
async get(key) {
|
|
2734
2829
|
this.ensureConnected();
|
|
@@ -3008,42 +3103,624 @@ var MemoryStorageAdapter = class extends BaseStorageAdapter {
|
|
|
3008
3103
|
}
|
|
3009
3104
|
};
|
|
3010
3105
|
|
|
3011
|
-
// libs/utils/src/storage/
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3106
|
+
// libs/utils/src/storage/adapters/filesystem.ts
|
|
3107
|
+
init_base();
|
|
3108
|
+
init_errors();
|
|
3109
|
+
init_pattern();
|
|
3110
|
+
var FileSystemStorageAdapter = class extends BaseStorageAdapter {
|
|
3111
|
+
backendName = "filesystem";
|
|
3112
|
+
baseDir;
|
|
3113
|
+
extension;
|
|
3114
|
+
dirMode;
|
|
3115
|
+
fileMode;
|
|
3116
|
+
constructor(options) {
|
|
3117
|
+
super();
|
|
3118
|
+
this.baseDir = options.baseDir;
|
|
3119
|
+
this.extension = options.extension ?? ".json";
|
|
3120
|
+
this.dirMode = options.dirMode ?? 448;
|
|
3121
|
+
this.fileMode = options.fileMode ?? 384;
|
|
3018
3122
|
}
|
|
3019
|
-
|
|
3020
|
-
|
|
3123
|
+
// ============================================
|
|
3124
|
+
// Connection Lifecycle
|
|
3125
|
+
// ============================================
|
|
3126
|
+
async connect() {
|
|
3127
|
+
if (this.connected) return;
|
|
3128
|
+
await ensureDir(this.baseDir);
|
|
3129
|
+
try {
|
|
3130
|
+
await mkdir(this.baseDir, { recursive: true, mode: this.dirMode });
|
|
3131
|
+
} catch {
|
|
3132
|
+
}
|
|
3133
|
+
this.connected = true;
|
|
3021
3134
|
}
|
|
3022
|
-
|
|
3023
|
-
return
|
|
3135
|
+
async disconnect() {
|
|
3136
|
+
if (!this.connected) return;
|
|
3137
|
+
this.connected = false;
|
|
3024
3138
|
}
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
return
|
|
3139
|
+
async ping() {
|
|
3140
|
+
if (!this.connected) return false;
|
|
3141
|
+
try {
|
|
3142
|
+
return await fileExists(this.baseDir);
|
|
3143
|
+
} catch {
|
|
3144
|
+
return false;
|
|
3031
3145
|
}
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3146
|
+
}
|
|
3147
|
+
// ============================================
|
|
3148
|
+
// Core Operations
|
|
3149
|
+
// ============================================
|
|
3150
|
+
async get(key) {
|
|
3151
|
+
this.ensureConnected();
|
|
3152
|
+
const filePath = this.keyToPath(key);
|
|
3153
|
+
try {
|
|
3154
|
+
const entry = await readJSON(filePath);
|
|
3155
|
+
if (!entry) return null;
|
|
3156
|
+
if (entry.expiresAt && Date.now() >= entry.expiresAt) {
|
|
3157
|
+
await this.deleteFile(filePath);
|
|
3158
|
+
return null;
|
|
3159
|
+
}
|
|
3160
|
+
return entry.value;
|
|
3161
|
+
} catch (e) {
|
|
3162
|
+
const error = e;
|
|
3163
|
+
if (error.code === "ENOENT") {
|
|
3164
|
+
return null;
|
|
3165
|
+
}
|
|
3166
|
+
throw e;
|
|
3035
3167
|
}
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3168
|
+
}
|
|
3169
|
+
async doSet(key, value, options) {
|
|
3170
|
+
const filePath = this.keyToPath(key);
|
|
3171
|
+
const existingEntry = await this.readEntry(filePath);
|
|
3172
|
+
const exists = existingEntry !== null && !this.isExpired(existingEntry);
|
|
3173
|
+
if (options?.ifNotExists && exists) {
|
|
3174
|
+
return;
|
|
3039
3175
|
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
return new UpstashStorageAdapter2(config.upstash);
|
|
3176
|
+
if (options?.ifExists && !exists) {
|
|
3177
|
+
return;
|
|
3043
3178
|
}
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3179
|
+
const entry = { value };
|
|
3180
|
+
if (options?.ttlSeconds) {
|
|
3181
|
+
entry.expiresAt = Date.now() + options.ttlSeconds * 1e3;
|
|
3182
|
+
}
|
|
3183
|
+
await this.atomicWrite(filePath, entry);
|
|
3184
|
+
}
|
|
3185
|
+
async delete(key) {
|
|
3186
|
+
this.ensureConnected();
|
|
3187
|
+
const filePath = this.keyToPath(key);
|
|
3188
|
+
return this.deleteFile(filePath);
|
|
3189
|
+
}
|
|
3190
|
+
async exists(key) {
|
|
3191
|
+
this.ensureConnected();
|
|
3192
|
+
const filePath = this.keyToPath(key);
|
|
3193
|
+
try {
|
|
3194
|
+
const entry = await this.readEntry(filePath);
|
|
3195
|
+
if (!entry) return false;
|
|
3196
|
+
if (this.isExpired(entry)) {
|
|
3197
|
+
await this.deleteFile(filePath);
|
|
3198
|
+
return false;
|
|
3199
|
+
}
|
|
3200
|
+
return true;
|
|
3201
|
+
} catch {
|
|
3202
|
+
return false;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
// ============================================
|
|
3206
|
+
// TTL Operations
|
|
3207
|
+
// ============================================
|
|
3208
|
+
async expire(key, ttlSeconds) {
|
|
3209
|
+
this.ensureConnected();
|
|
3210
|
+
const filePath = this.keyToPath(key);
|
|
3211
|
+
const entry = await this.readEntry(filePath);
|
|
3212
|
+
if (!entry || this.isExpired(entry)) {
|
|
3213
|
+
return false;
|
|
3214
|
+
}
|
|
3215
|
+
entry.expiresAt = Date.now() + ttlSeconds * 1e3;
|
|
3216
|
+
await this.atomicWrite(filePath, entry);
|
|
3217
|
+
return true;
|
|
3218
|
+
}
|
|
3219
|
+
async ttl(key) {
|
|
3220
|
+
this.ensureConnected();
|
|
3221
|
+
const filePath = this.keyToPath(key);
|
|
3222
|
+
const entry = await this.readEntry(filePath);
|
|
3223
|
+
if (!entry) return null;
|
|
3224
|
+
if (this.isExpired(entry)) {
|
|
3225
|
+
await this.deleteFile(filePath);
|
|
3226
|
+
return null;
|
|
3227
|
+
}
|
|
3228
|
+
if (entry.expiresAt === void 0) {
|
|
3229
|
+
return -1;
|
|
3230
|
+
}
|
|
3231
|
+
return Math.max(0, Math.ceil((entry.expiresAt - Date.now()) / 1e3));
|
|
3232
|
+
}
|
|
3233
|
+
// ============================================
|
|
3234
|
+
// Key Enumeration
|
|
3235
|
+
// ============================================
|
|
3236
|
+
async keys(pattern = "*") {
|
|
3237
|
+
this.ensureConnected();
|
|
3238
|
+
const regex = globToRegex(pattern);
|
|
3239
|
+
const result = [];
|
|
3240
|
+
try {
|
|
3241
|
+
const files = await readdir(this.baseDir);
|
|
3242
|
+
for (const file of files) {
|
|
3243
|
+
if (!file.endsWith(this.extension)) continue;
|
|
3244
|
+
const key = this.pathToKey(file);
|
|
3245
|
+
if (!regex.test(key)) continue;
|
|
3246
|
+
const filePath = this.keyToPath(key);
|
|
3247
|
+
const entry = await this.readEntry(filePath);
|
|
3248
|
+
if (entry && !this.isExpired(entry)) {
|
|
3249
|
+
result.push(key);
|
|
3250
|
+
} else if (entry && this.isExpired(entry)) {
|
|
3251
|
+
await this.deleteFile(filePath);
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
} catch (e) {
|
|
3255
|
+
const error = e;
|
|
3256
|
+
if (error.code === "ENOENT") {
|
|
3257
|
+
return [];
|
|
3258
|
+
}
|
|
3259
|
+
throw e;
|
|
3260
|
+
}
|
|
3261
|
+
return result;
|
|
3262
|
+
}
|
|
3263
|
+
// ============================================
|
|
3264
|
+
// Atomic Operations
|
|
3265
|
+
// ============================================
|
|
3266
|
+
async incr(key) {
|
|
3267
|
+
return this.incrBy(key, 1);
|
|
3268
|
+
}
|
|
3269
|
+
async decr(key) {
|
|
3270
|
+
return this.incrBy(key, -1);
|
|
3271
|
+
}
|
|
3272
|
+
async incrBy(key, amount) {
|
|
3273
|
+
this.ensureConnected();
|
|
3274
|
+
const filePath = this.keyToPath(key);
|
|
3275
|
+
const entry = await this.readEntry(filePath);
|
|
3276
|
+
let currentValue = 0;
|
|
3277
|
+
if (entry && !this.isExpired(entry)) {
|
|
3278
|
+
const parsed = parseInt(entry.value, 10);
|
|
3279
|
+
if (isNaN(parsed)) {
|
|
3280
|
+
throw new StorageOperationError("incrBy", key, "Value is not an integer");
|
|
3281
|
+
}
|
|
3282
|
+
currentValue = parsed;
|
|
3283
|
+
}
|
|
3284
|
+
const newValue = currentValue + amount;
|
|
3285
|
+
const newEntry = { value: String(newValue) };
|
|
3286
|
+
if (entry?.expiresAt && !this.isExpired(entry)) {
|
|
3287
|
+
newEntry.expiresAt = entry.expiresAt;
|
|
3288
|
+
}
|
|
3289
|
+
await this.atomicWrite(filePath, newEntry);
|
|
3290
|
+
return newValue;
|
|
3291
|
+
}
|
|
3292
|
+
// ============================================
|
|
3293
|
+
// Internal Helpers
|
|
3294
|
+
// ============================================
|
|
3295
|
+
/**
|
|
3296
|
+
* Convert a storage key to a file path.
|
|
3297
|
+
* Sanitizes the key to be filesystem-safe.
|
|
3298
|
+
*/
|
|
3299
|
+
keyToPath(key) {
|
|
3300
|
+
const safeKey = this.encodeKey(key);
|
|
3301
|
+
return `${this.baseDir}/${safeKey}${this.extension}`;
|
|
3302
|
+
}
|
|
3303
|
+
/**
|
|
3304
|
+
* Convert a filename back to a storage key.
|
|
3305
|
+
*/
|
|
3306
|
+
pathToKey(filename) {
|
|
3307
|
+
const safeKey = filename.slice(0, -this.extension.length);
|
|
3308
|
+
return this.decodeKey(safeKey);
|
|
3309
|
+
}
|
|
3310
|
+
/**
|
|
3311
|
+
* Encode a key to be filesystem-safe.
|
|
3312
|
+
* Uses URL encoding to handle special characters.
|
|
3313
|
+
*/
|
|
3314
|
+
encodeKey(key) {
|
|
3315
|
+
return encodeURIComponent(key).replace(/\./g, "%2E");
|
|
3316
|
+
}
|
|
3317
|
+
/**
|
|
3318
|
+
* Decode a filesystem-safe key back to original.
|
|
3319
|
+
*/
|
|
3320
|
+
decodeKey(encoded) {
|
|
3321
|
+
return decodeURIComponent(encoded);
|
|
3322
|
+
}
|
|
3323
|
+
/**
|
|
3324
|
+
* Read an entry from a file.
|
|
3325
|
+
*/
|
|
3326
|
+
async readEntry(filePath) {
|
|
3327
|
+
try {
|
|
3328
|
+
return await readJSON(filePath);
|
|
3329
|
+
} catch {
|
|
3330
|
+
return null;
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
/**
|
|
3334
|
+
* Check if an entry is expired.
|
|
3335
|
+
*/
|
|
3336
|
+
isExpired(entry) {
|
|
3337
|
+
return entry.expiresAt !== void 0 && Date.now() >= entry.expiresAt;
|
|
3338
|
+
}
|
|
3339
|
+
/**
|
|
3340
|
+
* Delete a file, returning true if it existed.
|
|
3341
|
+
*/
|
|
3342
|
+
async deleteFile(filePath) {
|
|
3343
|
+
try {
|
|
3344
|
+
await unlink(filePath);
|
|
3345
|
+
return true;
|
|
3346
|
+
} catch (e) {
|
|
3347
|
+
const error = e;
|
|
3348
|
+
if (error.code === "ENOENT") {
|
|
3349
|
+
return false;
|
|
3350
|
+
}
|
|
3351
|
+
throw e;
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
/**
|
|
3355
|
+
* Atomic write: write to temp file then rename.
|
|
3356
|
+
* This ensures we don't corrupt the file if write is interrupted.
|
|
3357
|
+
*/
|
|
3358
|
+
async atomicWrite(filePath, entry) {
|
|
3359
|
+
const tempSuffix = bytesToHex(randomBytes(8));
|
|
3360
|
+
const tempPath = `${filePath}.${tempSuffix}.tmp`;
|
|
3361
|
+
try {
|
|
3362
|
+
const content = JSON.stringify(entry, null, 2);
|
|
3363
|
+
await writeFile(tempPath, content, { mode: this.fileMode });
|
|
3364
|
+
await rename(tempPath, filePath);
|
|
3365
|
+
} catch (e) {
|
|
3366
|
+
try {
|
|
3367
|
+
await unlink(tempPath);
|
|
3368
|
+
} catch {
|
|
3369
|
+
}
|
|
3370
|
+
throw e;
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
/**
|
|
3374
|
+
* Get suggestion message for pub/sub not supported error.
|
|
3375
|
+
*/
|
|
3376
|
+
getPubSubSuggestion() {
|
|
3377
|
+
return "FileSystem adapter does not support pub/sub. Use Redis or Memory adapter for pub/sub support.";
|
|
3378
|
+
}
|
|
3379
|
+
};
|
|
3380
|
+
|
|
3381
|
+
// libs/utils/src/crypto/key-persistence/factory.ts
|
|
3382
|
+
var DEFAULT_BASE_DIR = ".frontmcp/keys";
|
|
3383
|
+
async function createKeyPersistence(options) {
|
|
3384
|
+
const type = options?.type ?? "auto";
|
|
3385
|
+
const baseDir = options?.baseDir ?? DEFAULT_BASE_DIR;
|
|
3386
|
+
let adapter;
|
|
3387
|
+
if (type === "memory") {
|
|
3388
|
+
adapter = new MemoryStorageAdapter();
|
|
3389
|
+
} else if (type === "filesystem") {
|
|
3390
|
+
adapter = new FileSystemStorageAdapter({ baseDir });
|
|
3391
|
+
} else {
|
|
3392
|
+
if (isNode()) {
|
|
3393
|
+
adapter = new FileSystemStorageAdapter({ baseDir });
|
|
3394
|
+
} else {
|
|
3395
|
+
adapter = new MemoryStorageAdapter();
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
await adapter.connect();
|
|
3399
|
+
return new KeyPersistence({
|
|
3400
|
+
storage: adapter,
|
|
3401
|
+
throwOnInvalid: options?.throwOnInvalid ?? false,
|
|
3402
|
+
enableCache: options?.enableCache ?? true
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
function createKeyPersistenceWithStorage(storage, options) {
|
|
3406
|
+
return new KeyPersistence({
|
|
3407
|
+
storage,
|
|
3408
|
+
throwOnInvalid: options?.throwOnInvalid ?? false,
|
|
3409
|
+
enableCache: options?.enableCache ?? true
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
// libs/utils/src/crypto/index.ts
|
|
3414
|
+
var _provider = null;
|
|
3415
|
+
function getCrypto() {
|
|
3416
|
+
if (!_provider) {
|
|
3417
|
+
if (isNode()) {
|
|
3418
|
+
_provider = (init_node(), __toCommonJS(node_exports)).nodeCrypto;
|
|
3419
|
+
} else {
|
|
3420
|
+
_provider = (init_browser(), __toCommonJS(browser_exports)).browserCrypto;
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
return _provider;
|
|
3424
|
+
}
|
|
3425
|
+
function rsaVerify2(jwtAlg, data, publicJwk, signature) {
|
|
3426
|
+
assertNode("rsaVerify");
|
|
3427
|
+
return (init_node(), __toCommonJS(node_exports)).rsaVerify(jwtAlg, data, publicJwk, signature);
|
|
3428
|
+
}
|
|
3429
|
+
function randomUUID() {
|
|
3430
|
+
return getCrypto().randomUUID();
|
|
3431
|
+
}
|
|
3432
|
+
function randomBytes(length) {
|
|
3433
|
+
if (!Number.isInteger(length) || length <= 0) {
|
|
3434
|
+
throw new Error(`randomBytes length must be a positive integer, got ${length}`);
|
|
3435
|
+
}
|
|
3436
|
+
return getCrypto().randomBytes(length);
|
|
3437
|
+
}
|
|
3438
|
+
function sha256(data) {
|
|
3439
|
+
return getCrypto().sha256(data);
|
|
3440
|
+
}
|
|
3441
|
+
function sha256Hex(data) {
|
|
3442
|
+
return getCrypto().sha256Hex(data);
|
|
3443
|
+
}
|
|
3444
|
+
function hmacSha256(key, data) {
|
|
3445
|
+
return getCrypto().hmacSha256(key, data);
|
|
3446
|
+
}
|
|
3447
|
+
var HKDF_SHA256_MAX_LENGTH = 255 * 32;
|
|
3448
|
+
function hkdfSha256(ikm, salt, info, length) {
|
|
3449
|
+
if (!Number.isInteger(length) || length <= 0) {
|
|
3450
|
+
throw new Error(`HKDF length must be a positive integer, got ${length}`);
|
|
3451
|
+
}
|
|
3452
|
+
if (length > HKDF_SHA256_MAX_LENGTH) {
|
|
3453
|
+
throw new Error(`HKDF-SHA256 length cannot exceed ${HKDF_SHA256_MAX_LENGTH} bytes, got ${length}`);
|
|
3454
|
+
}
|
|
3455
|
+
return getCrypto().hkdfSha256(ikm, salt, info, length);
|
|
3456
|
+
}
|
|
3457
|
+
function encryptAesGcm(key, plaintext, iv) {
|
|
3458
|
+
if (key.length !== 32) {
|
|
3459
|
+
throw new Error(`AES-256-GCM requires a 32-byte key, got ${key.length} bytes`);
|
|
3460
|
+
}
|
|
3461
|
+
if (iv.length !== 12) {
|
|
3462
|
+
throw new Error(`AES-GCM requires a 12-byte IV, got ${iv.length} bytes`);
|
|
3463
|
+
}
|
|
3464
|
+
return getCrypto().encryptAesGcm(key, plaintext, iv);
|
|
3465
|
+
}
|
|
3466
|
+
function decryptAesGcm(key, ciphertext, iv, tag) {
|
|
3467
|
+
if (key.length !== 32) {
|
|
3468
|
+
throw new Error(`AES-256-GCM requires a 32-byte key, got ${key.length} bytes`);
|
|
3469
|
+
}
|
|
3470
|
+
if (iv.length !== 12) {
|
|
3471
|
+
throw new Error(`AES-GCM requires a 12-byte IV, got ${iv.length} bytes`);
|
|
3472
|
+
}
|
|
3473
|
+
if (tag.length !== 16) {
|
|
3474
|
+
throw new Error(`AES-GCM requires a 16-byte authentication tag, got ${tag.length} bytes`);
|
|
3475
|
+
}
|
|
3476
|
+
return getCrypto().decryptAesGcm(key, ciphertext, iv, tag);
|
|
3477
|
+
}
|
|
3478
|
+
function timingSafeEqual(a, b) {
|
|
3479
|
+
if (a.length !== b.length) {
|
|
3480
|
+
throw new Error(`timingSafeEqual requires equal-length arrays, got ${a.length} and ${b.length} bytes`);
|
|
3481
|
+
}
|
|
3482
|
+
return getCrypto().timingSafeEqual(a, b);
|
|
3483
|
+
}
|
|
3484
|
+
function bytesToHex(data) {
|
|
3485
|
+
return Array.from(data).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3486
|
+
}
|
|
3487
|
+
function base64urlEncode(data) {
|
|
3488
|
+
let base64;
|
|
3489
|
+
if (typeof Buffer !== "undefined") {
|
|
3490
|
+
base64 = Buffer.from(data).toString("base64");
|
|
3491
|
+
} else {
|
|
3492
|
+
const binString = Array.from(data, (byte) => String.fromCodePoint(byte)).join("");
|
|
3493
|
+
base64 = btoa(binString);
|
|
3494
|
+
}
|
|
3495
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
3496
|
+
}
|
|
3497
|
+
function base64urlDecode(data) {
|
|
3498
|
+
let base64 = data.replace(/-/g, "+").replace(/_/g, "/");
|
|
3499
|
+
const pad = base64.length % 4;
|
|
3500
|
+
if (pad) {
|
|
3501
|
+
base64 += "=".repeat(4 - pad);
|
|
3502
|
+
}
|
|
3503
|
+
if (typeof Buffer !== "undefined") {
|
|
3504
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
3505
|
+
} else {
|
|
3506
|
+
const binString = atob(base64);
|
|
3507
|
+
return Uint8Array.from(binString, (c) => c.codePointAt(0) ?? 0);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
function base64Encode(data) {
|
|
3511
|
+
if (typeof Buffer !== "undefined") {
|
|
3512
|
+
return Buffer.from(data).toString("base64");
|
|
3513
|
+
} else {
|
|
3514
|
+
const binString = Array.from(data, (byte) => String.fromCodePoint(byte)).join("");
|
|
3515
|
+
return btoa(binString);
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
function base64Decode(data) {
|
|
3519
|
+
if (typeof Buffer !== "undefined") {
|
|
3520
|
+
return new Uint8Array(Buffer.from(data, "base64"));
|
|
3521
|
+
} else {
|
|
3522
|
+
const binString = atob(data);
|
|
3523
|
+
return Uint8Array.from(binString, (c) => c.codePointAt(0) ?? 0);
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
function sha256Base64url(data) {
|
|
3527
|
+
return base64urlEncode(sha256(data));
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
// libs/utils/src/storage/factory.ts
|
|
3531
|
+
init_errors();
|
|
3532
|
+
|
|
3533
|
+
// libs/utils/src/storage/namespace.ts
|
|
3534
|
+
var NAMESPACE_SEPARATOR = ":";
|
|
3535
|
+
function buildPrefix(name, id) {
|
|
3536
|
+
if (id) {
|
|
3537
|
+
return `${name}${NAMESPACE_SEPARATOR}${id}${NAMESPACE_SEPARATOR}`;
|
|
3538
|
+
}
|
|
3539
|
+
return `${name}${NAMESPACE_SEPARATOR}`;
|
|
3540
|
+
}
|
|
3541
|
+
var NamespacedStorageImpl = class _NamespacedStorageImpl {
|
|
3542
|
+
constructor(adapter, prefix = "", root = adapter) {
|
|
3543
|
+
this.adapter = adapter;
|
|
3544
|
+
this.prefix = prefix;
|
|
3545
|
+
this.root = root;
|
|
3546
|
+
}
|
|
3547
|
+
// ============================================
|
|
3548
|
+
// Key Prefixing Helpers
|
|
3549
|
+
// ============================================
|
|
3550
|
+
/**
|
|
3551
|
+
* Add prefix to a key.
|
|
3552
|
+
*/
|
|
3553
|
+
prefixKey(key) {
|
|
3554
|
+
return this.prefix + key;
|
|
3555
|
+
}
|
|
3556
|
+
/**
|
|
3557
|
+
* Remove prefix from a key.
|
|
3558
|
+
*/
|
|
3559
|
+
unprefixKey(key) {
|
|
3560
|
+
if (this.prefix && key.startsWith(this.prefix)) {
|
|
3561
|
+
return key.slice(this.prefix.length);
|
|
3562
|
+
}
|
|
3563
|
+
return key;
|
|
3564
|
+
}
|
|
3565
|
+
/**
|
|
3566
|
+
* Add prefix to a pattern (for keys() operation).
|
|
3567
|
+
*/
|
|
3568
|
+
prefixPattern(pattern) {
|
|
3569
|
+
return this.prefix + pattern;
|
|
3570
|
+
}
|
|
3571
|
+
/**
|
|
3572
|
+
* Add prefix to a channel (for pub/sub).
|
|
3573
|
+
*/
|
|
3574
|
+
prefixChannel(channel) {
|
|
3575
|
+
return this.prefix + channel;
|
|
3576
|
+
}
|
|
3577
|
+
// ============================================
|
|
3578
|
+
// Namespace API
|
|
3579
|
+
// ============================================
|
|
3580
|
+
namespace(name, id) {
|
|
3581
|
+
const newPrefix = this.prefix + buildPrefix(name, id);
|
|
3582
|
+
return new _NamespacedStorageImpl(this.adapter, newPrefix, this.root);
|
|
3583
|
+
}
|
|
3584
|
+
// ============================================
|
|
3585
|
+
// Connection Lifecycle (delegated to root)
|
|
3586
|
+
// ============================================
|
|
3587
|
+
async connect() {
|
|
3588
|
+
return this.adapter.connect();
|
|
3589
|
+
}
|
|
3590
|
+
async disconnect() {
|
|
3591
|
+
return this.adapter.disconnect();
|
|
3592
|
+
}
|
|
3593
|
+
async ping() {
|
|
3594
|
+
return this.adapter.ping();
|
|
3595
|
+
}
|
|
3596
|
+
// ============================================
|
|
3597
|
+
// Core Operations (with prefixing)
|
|
3598
|
+
// ============================================
|
|
3599
|
+
async get(key) {
|
|
3600
|
+
return this.adapter.get(this.prefixKey(key));
|
|
3601
|
+
}
|
|
3602
|
+
async set(key, value, options) {
|
|
3603
|
+
return this.adapter.set(this.prefixKey(key), value, options);
|
|
3604
|
+
}
|
|
3605
|
+
async delete(key) {
|
|
3606
|
+
return this.adapter.delete(this.prefixKey(key));
|
|
3607
|
+
}
|
|
3608
|
+
async exists(key) {
|
|
3609
|
+
return this.adapter.exists(this.prefixKey(key));
|
|
3610
|
+
}
|
|
3611
|
+
// ============================================
|
|
3612
|
+
// Batch Operations (with prefixing)
|
|
3613
|
+
// ============================================
|
|
3614
|
+
async mget(keys) {
|
|
3615
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
3616
|
+
return this.adapter.mget(prefixedKeys);
|
|
3617
|
+
}
|
|
3618
|
+
async mset(entries) {
|
|
3619
|
+
const prefixedEntries = entries.map((e) => ({
|
|
3620
|
+
...e,
|
|
3621
|
+
key: this.prefixKey(e.key)
|
|
3622
|
+
}));
|
|
3623
|
+
return this.adapter.mset(prefixedEntries);
|
|
3624
|
+
}
|
|
3625
|
+
async mdelete(keys) {
|
|
3626
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
3627
|
+
return this.adapter.mdelete(prefixedKeys);
|
|
3628
|
+
}
|
|
3629
|
+
// ============================================
|
|
3630
|
+
// TTL Operations (with prefixing)
|
|
3631
|
+
// ============================================
|
|
3632
|
+
async expire(key, ttlSeconds) {
|
|
3633
|
+
return this.adapter.expire(this.prefixKey(key), ttlSeconds);
|
|
3634
|
+
}
|
|
3635
|
+
async ttl(key) {
|
|
3636
|
+
return this.adapter.ttl(this.prefixKey(key));
|
|
3637
|
+
}
|
|
3638
|
+
// ============================================
|
|
3639
|
+
// Key Enumeration (with prefixing)
|
|
3640
|
+
// ============================================
|
|
3641
|
+
async keys(pattern = "*") {
|
|
3642
|
+
const prefixedPattern = this.prefixPattern(pattern);
|
|
3643
|
+
const keys = await this.adapter.keys(prefixedPattern);
|
|
3644
|
+
return keys.map((k) => this.unprefixKey(k));
|
|
3645
|
+
}
|
|
3646
|
+
async count(pattern = "*") {
|
|
3647
|
+
const prefixedPattern = this.prefixPattern(pattern);
|
|
3648
|
+
return this.adapter.count(prefixedPattern);
|
|
3649
|
+
}
|
|
3650
|
+
// ============================================
|
|
3651
|
+
// Atomic Operations (with prefixing)
|
|
3652
|
+
// ============================================
|
|
3653
|
+
async incr(key) {
|
|
3654
|
+
return this.adapter.incr(this.prefixKey(key));
|
|
3655
|
+
}
|
|
3656
|
+
async decr(key) {
|
|
3657
|
+
return this.adapter.decr(this.prefixKey(key));
|
|
3658
|
+
}
|
|
3659
|
+
async incrBy(key, amount) {
|
|
3660
|
+
return this.adapter.incrBy(this.prefixKey(key), amount);
|
|
3661
|
+
}
|
|
3662
|
+
// ============================================
|
|
3663
|
+
// Pub/Sub (with channel prefixing)
|
|
3664
|
+
// ============================================
|
|
3665
|
+
async publish(channel, message) {
|
|
3666
|
+
return this.adapter.publish(this.prefixChannel(channel), message);
|
|
3667
|
+
}
|
|
3668
|
+
async subscribe(channel, handler) {
|
|
3669
|
+
const prefixedChannel = this.prefixChannel(channel);
|
|
3670
|
+
const wrappedHandler = (message, ch) => {
|
|
3671
|
+
const unprefixedChannel = ch.startsWith(this.prefix) ? ch.slice(this.prefix.length) : ch;
|
|
3672
|
+
handler(message, unprefixedChannel);
|
|
3673
|
+
};
|
|
3674
|
+
return this.adapter.subscribe(prefixedChannel, wrappedHandler);
|
|
3675
|
+
}
|
|
3676
|
+
supportsPubSub() {
|
|
3677
|
+
return this.adapter.supportsPubSub();
|
|
3678
|
+
}
|
|
3679
|
+
};
|
|
3680
|
+
function createRootStorage(adapter) {
|
|
3681
|
+
return new NamespacedStorageImpl(adapter, "", adapter);
|
|
3682
|
+
}
|
|
3683
|
+
function createNamespacedStorage(adapter, prefix) {
|
|
3684
|
+
const normalizedPrefix = prefix && !prefix.endsWith(NAMESPACE_SEPARATOR) ? prefix + NAMESPACE_SEPARATOR : prefix;
|
|
3685
|
+
return new NamespacedStorageImpl(adapter, normalizedPrefix, adapter);
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
// libs/utils/src/storage/factory.ts
|
|
3689
|
+
function isProduction() {
|
|
3690
|
+
return process.env["NODE_ENV"] === "production";
|
|
3691
|
+
}
|
|
3692
|
+
function detectStorageType() {
|
|
3693
|
+
if (process.env["UPSTASH_REDIS_REST_URL"] && process.env["UPSTASH_REDIS_REST_TOKEN"]) {
|
|
3694
|
+
return "upstash";
|
|
3695
|
+
}
|
|
3696
|
+
if (process.env["KV_REST_API_URL"] && process.env["KV_REST_API_TOKEN"]) {
|
|
3697
|
+
return "vercel-kv";
|
|
3698
|
+
}
|
|
3699
|
+
if (process.env["REDIS_URL"] || process.env["REDIS_HOST"]) {
|
|
3700
|
+
return "redis";
|
|
3701
|
+
}
|
|
3702
|
+
return "memory";
|
|
3703
|
+
}
|
|
3704
|
+
async function createAdapter(type, config) {
|
|
3705
|
+
switch (type) {
|
|
3706
|
+
case "memory": {
|
|
3707
|
+
return new MemoryStorageAdapter(config.memory);
|
|
3708
|
+
}
|
|
3709
|
+
case "redis": {
|
|
3710
|
+
const { RedisStorageAdapter: RedisStorageAdapter2 } = await Promise.resolve().then(() => (init_redis(), redis_exports));
|
|
3711
|
+
return new RedisStorageAdapter2(config.redis);
|
|
3712
|
+
}
|
|
3713
|
+
case "vercel-kv": {
|
|
3714
|
+
const { VercelKvStorageAdapter: VercelKvStorageAdapter2 } = await Promise.resolve().then(() => (init_vercel_kv(), vercel_kv_exports));
|
|
3715
|
+
return new VercelKvStorageAdapter2(config.vercelKv);
|
|
3716
|
+
}
|
|
3717
|
+
case "upstash": {
|
|
3718
|
+
const { UpstashStorageAdapter: UpstashStorageAdapter2 } = await Promise.resolve().then(() => (init_upstash(), upstash_exports));
|
|
3719
|
+
return new UpstashStorageAdapter2(config.upstash);
|
|
3720
|
+
}
|
|
3721
|
+
case "auto":
|
|
3722
|
+
throw new StorageConfigError("auto", "Auto type should be resolved before calling createAdapter");
|
|
3723
|
+
default:
|
|
3047
3724
|
throw new StorageConfigError("unknown", `Unknown storage type: ${type}`);
|
|
3048
3725
|
}
|
|
3049
3726
|
}
|
|
@@ -3103,6 +3780,501 @@ function getDetectedStorageType() {
|
|
|
3103
3780
|
// libs/utils/src/storage/index.ts
|
|
3104
3781
|
init_errors();
|
|
3105
3782
|
|
|
3783
|
+
// libs/utils/src/storage/typed-storage.ts
|
|
3784
|
+
var TypedStorage = class {
|
|
3785
|
+
storage;
|
|
3786
|
+
serialize;
|
|
3787
|
+
deserialize;
|
|
3788
|
+
schema;
|
|
3789
|
+
throwOnInvalid;
|
|
3790
|
+
constructor(storage, options = {}) {
|
|
3791
|
+
this.storage = storage;
|
|
3792
|
+
this.serialize = options.serialize ?? JSON.stringify;
|
|
3793
|
+
this.deserialize = options.deserialize ?? JSON.parse;
|
|
3794
|
+
this.schema = options.schema;
|
|
3795
|
+
this.throwOnInvalid = options.throwOnInvalid ?? false;
|
|
3796
|
+
}
|
|
3797
|
+
/**
|
|
3798
|
+
* Get a typed value by key.
|
|
3799
|
+
*
|
|
3800
|
+
* @param key - Storage key
|
|
3801
|
+
* @returns The typed value, or null if not found or invalid
|
|
3802
|
+
*/
|
|
3803
|
+
async get(key) {
|
|
3804
|
+
const raw = await this.storage.get(key);
|
|
3805
|
+
if (raw === null) {
|
|
3806
|
+
return null;
|
|
3807
|
+
}
|
|
3808
|
+
return this.parseValue(raw, key);
|
|
3809
|
+
}
|
|
3810
|
+
/**
|
|
3811
|
+
* Set a typed value with optional TTL.
|
|
3812
|
+
*
|
|
3813
|
+
* @param key - Storage key
|
|
3814
|
+
* @param value - Typed value to store
|
|
3815
|
+
* @param options - Optional TTL and conditional flags
|
|
3816
|
+
*/
|
|
3817
|
+
async set(key, value, options) {
|
|
3818
|
+
const serialized = this.serialize(value);
|
|
3819
|
+
await this.storage.set(key, serialized, options);
|
|
3820
|
+
}
|
|
3821
|
+
/**
|
|
3822
|
+
* Delete a key.
|
|
3823
|
+
*
|
|
3824
|
+
* @param key - Storage key
|
|
3825
|
+
* @returns true if key existed and was deleted
|
|
3826
|
+
*/
|
|
3827
|
+
async delete(key) {
|
|
3828
|
+
return this.storage.delete(key);
|
|
3829
|
+
}
|
|
3830
|
+
/**
|
|
3831
|
+
* Check if a key exists.
|
|
3832
|
+
*
|
|
3833
|
+
* @param key - Storage key
|
|
3834
|
+
* @returns true if key exists
|
|
3835
|
+
*/
|
|
3836
|
+
async exists(key) {
|
|
3837
|
+
return this.storage.exists(key);
|
|
3838
|
+
}
|
|
3839
|
+
/**
|
|
3840
|
+
* Get multiple typed values.
|
|
3841
|
+
*
|
|
3842
|
+
* @param keys - Array of storage keys
|
|
3843
|
+
* @returns Array of typed values (null for missing/invalid keys)
|
|
3844
|
+
*/
|
|
3845
|
+
async mget(keys) {
|
|
3846
|
+
if (keys.length === 0) {
|
|
3847
|
+
return [];
|
|
3848
|
+
}
|
|
3849
|
+
const rawValues = await this.storage.mget(keys);
|
|
3850
|
+
return rawValues.map((raw, index) => {
|
|
3851
|
+
if (raw === null) {
|
|
3852
|
+
return null;
|
|
3853
|
+
}
|
|
3854
|
+
return this.parseValue(raw, keys[index]);
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
/**
|
|
3858
|
+
* Set multiple typed values.
|
|
3859
|
+
*
|
|
3860
|
+
* @param entries - Array of key-value-options entries
|
|
3861
|
+
*/
|
|
3862
|
+
async mset(entries) {
|
|
3863
|
+
if (entries.length === 0) {
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
3866
|
+
const rawEntries = entries.map((entry) => ({
|
|
3867
|
+
key: entry.key,
|
|
3868
|
+
value: this.serialize(entry.value),
|
|
3869
|
+
options: entry.options
|
|
3870
|
+
}));
|
|
3871
|
+
await this.storage.mset(rawEntries);
|
|
3872
|
+
}
|
|
3873
|
+
/**
|
|
3874
|
+
* Delete multiple keys.
|
|
3875
|
+
*
|
|
3876
|
+
* @param keys - Array of storage keys
|
|
3877
|
+
* @returns Number of keys actually deleted
|
|
3878
|
+
*/
|
|
3879
|
+
async mdelete(keys) {
|
|
3880
|
+
return this.storage.mdelete(keys);
|
|
3881
|
+
}
|
|
3882
|
+
/**
|
|
3883
|
+
* Update TTL on an existing key.
|
|
3884
|
+
*
|
|
3885
|
+
* @param key - Storage key
|
|
3886
|
+
* @param ttlSeconds - New TTL in seconds
|
|
3887
|
+
* @returns true if key exists and TTL was set
|
|
3888
|
+
*/
|
|
3889
|
+
async expire(key, ttlSeconds) {
|
|
3890
|
+
return this.storage.expire(key, ttlSeconds);
|
|
3891
|
+
}
|
|
3892
|
+
/**
|
|
3893
|
+
* Get remaining TTL for a key.
|
|
3894
|
+
*
|
|
3895
|
+
* @param key - Storage key
|
|
3896
|
+
* @returns TTL in seconds, -1 if no TTL, or null if key doesn't exist
|
|
3897
|
+
*/
|
|
3898
|
+
async ttl(key) {
|
|
3899
|
+
return this.storage.ttl(key);
|
|
3900
|
+
}
|
|
3901
|
+
/**
|
|
3902
|
+
* List keys matching a pattern.
|
|
3903
|
+
*
|
|
3904
|
+
* @param pattern - Glob pattern (default: '*' for all keys)
|
|
3905
|
+
* @returns Array of matching keys
|
|
3906
|
+
*/
|
|
3907
|
+
async keys(pattern) {
|
|
3908
|
+
return this.storage.keys(pattern);
|
|
3909
|
+
}
|
|
3910
|
+
/**
|
|
3911
|
+
* Count keys matching a pattern.
|
|
3912
|
+
*
|
|
3913
|
+
* @param pattern - Glob pattern (default: '*' for all keys)
|
|
3914
|
+
* @returns Number of matching keys
|
|
3915
|
+
*/
|
|
3916
|
+
async count(pattern) {
|
|
3917
|
+
return this.storage.count(pattern);
|
|
3918
|
+
}
|
|
3919
|
+
/**
|
|
3920
|
+
* Get the underlying storage adapter.
|
|
3921
|
+
* Use with caution - operations bypass type safety.
|
|
3922
|
+
*/
|
|
3923
|
+
get raw() {
|
|
3924
|
+
return this.storage;
|
|
3925
|
+
}
|
|
3926
|
+
/**
|
|
3927
|
+
* Parse and validate a raw value.
|
|
3928
|
+
*/
|
|
3929
|
+
parseValue(raw, key) {
|
|
3930
|
+
try {
|
|
3931
|
+
const parsed = this.deserialize(raw);
|
|
3932
|
+
if (this.schema) {
|
|
3933
|
+
const result = this.schema.safeParse(parsed);
|
|
3934
|
+
if (!result.success) {
|
|
3935
|
+
if (this.throwOnInvalid) {
|
|
3936
|
+
throw new Error(`TypedStorage validation failed for key "${key}": ${result.error.message}`);
|
|
3937
|
+
}
|
|
3938
|
+
return null;
|
|
3939
|
+
}
|
|
3940
|
+
return result.data;
|
|
3941
|
+
}
|
|
3942
|
+
return parsed;
|
|
3943
|
+
} catch (error) {
|
|
3944
|
+
if (this.throwOnInvalid) {
|
|
3945
|
+
throw error;
|
|
3946
|
+
}
|
|
3947
|
+
return null;
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
};
|
|
3951
|
+
|
|
3952
|
+
// libs/utils/src/storage/encrypted-typed-storage.ts
|
|
3953
|
+
init_errors();
|
|
3954
|
+
var textEncoder2 = new TextEncoder();
|
|
3955
|
+
var textDecoder2 = new TextDecoder();
|
|
3956
|
+
var EncryptedTypedStorage = class {
|
|
3957
|
+
storage;
|
|
3958
|
+
activeKey;
|
|
3959
|
+
keyMap;
|
|
3960
|
+
schema;
|
|
3961
|
+
throwOnError;
|
|
3962
|
+
onKeyRotationNeeded;
|
|
3963
|
+
clientBinding;
|
|
3964
|
+
constructor(storage, options) {
|
|
3965
|
+
if (!options.keys || options.keys.length === 0) {
|
|
3966
|
+
throw new EncryptedStorageError("At least one encryption key is required");
|
|
3967
|
+
}
|
|
3968
|
+
for (const k of options.keys) {
|
|
3969
|
+
if (k.key.length !== 32) {
|
|
3970
|
+
throw new EncryptedStorageError(
|
|
3971
|
+
`Encryption key "${k.kid}" must be 32 bytes (AES-256), got ${k.key.length} bytes`
|
|
3972
|
+
);
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
this.storage = storage;
|
|
3976
|
+
this.activeKey = options.keys[0];
|
|
3977
|
+
this.keyMap = new Map(options.keys.map((k) => [k.kid, k.key]));
|
|
3978
|
+
this.schema = options.schema;
|
|
3979
|
+
this.throwOnError = options.throwOnError ?? false;
|
|
3980
|
+
this.onKeyRotationNeeded = options.onKeyRotationNeeded;
|
|
3981
|
+
this.clientBinding = options.clientBinding;
|
|
3982
|
+
}
|
|
3983
|
+
/**
|
|
3984
|
+
* Get a decrypted value by key.
|
|
3985
|
+
*
|
|
3986
|
+
* @param key - Storage key
|
|
3987
|
+
* @returns The decrypted value, or null if not found or decryption fails
|
|
3988
|
+
*/
|
|
3989
|
+
async get(key) {
|
|
3990
|
+
const raw = await this.storage.get(key);
|
|
3991
|
+
if (raw === null) {
|
|
3992
|
+
return null;
|
|
3993
|
+
}
|
|
3994
|
+
return this.decryptAndParse(raw, key);
|
|
3995
|
+
}
|
|
3996
|
+
/**
|
|
3997
|
+
* Encrypt and store a value.
|
|
3998
|
+
*
|
|
3999
|
+
* @param key - Storage key
|
|
4000
|
+
* @param value - Value to encrypt and store
|
|
4001
|
+
* @param options - Optional TTL and conditional flags
|
|
4002
|
+
*/
|
|
4003
|
+
async set(key, value, options) {
|
|
4004
|
+
const encrypted = this.encryptValue(value);
|
|
4005
|
+
const serialized = JSON.stringify(encrypted);
|
|
4006
|
+
await this.storage.set(key, serialized, options);
|
|
4007
|
+
}
|
|
4008
|
+
/**
|
|
4009
|
+
* Delete a key.
|
|
4010
|
+
*
|
|
4011
|
+
* @param key - Storage key
|
|
4012
|
+
* @returns true if key existed and was deleted
|
|
4013
|
+
*/
|
|
4014
|
+
async delete(key) {
|
|
4015
|
+
return this.storage.delete(key);
|
|
4016
|
+
}
|
|
4017
|
+
/**
|
|
4018
|
+
* Check if a key exists.
|
|
4019
|
+
*
|
|
4020
|
+
* @param key - Storage key
|
|
4021
|
+
* @returns true if key exists
|
|
4022
|
+
*/
|
|
4023
|
+
async exists(key) {
|
|
4024
|
+
return this.storage.exists(key);
|
|
4025
|
+
}
|
|
4026
|
+
/**
|
|
4027
|
+
* Get multiple decrypted values.
|
|
4028
|
+
*
|
|
4029
|
+
* @param keys - Array of storage keys
|
|
4030
|
+
* @returns Array of decrypted values (null for missing/invalid keys)
|
|
4031
|
+
*/
|
|
4032
|
+
async mget(keys) {
|
|
4033
|
+
if (keys.length === 0) {
|
|
4034
|
+
return [];
|
|
4035
|
+
}
|
|
4036
|
+
const rawValues = await this.storage.mget(keys);
|
|
4037
|
+
return rawValues.map((raw, index) => {
|
|
4038
|
+
if (raw === null) {
|
|
4039
|
+
return null;
|
|
4040
|
+
}
|
|
4041
|
+
return this.decryptAndParse(raw, keys[index]);
|
|
4042
|
+
});
|
|
4043
|
+
}
|
|
4044
|
+
/**
|
|
4045
|
+
* Encrypt and store multiple values.
|
|
4046
|
+
*
|
|
4047
|
+
* @param entries - Array of key-value-options entries
|
|
4048
|
+
*/
|
|
4049
|
+
async mset(entries) {
|
|
4050
|
+
if (entries.length === 0) {
|
|
4051
|
+
return;
|
|
4052
|
+
}
|
|
4053
|
+
const rawEntries = entries.map((entry) => ({
|
|
4054
|
+
key: entry.key,
|
|
4055
|
+
value: JSON.stringify(this.encryptValue(entry.value)),
|
|
4056
|
+
options: entry.options
|
|
4057
|
+
}));
|
|
4058
|
+
await this.storage.mset(rawEntries);
|
|
4059
|
+
}
|
|
4060
|
+
/**
|
|
4061
|
+
* Delete multiple keys.
|
|
4062
|
+
*
|
|
4063
|
+
* @param keys - Array of storage keys
|
|
4064
|
+
* @returns Number of keys actually deleted
|
|
4065
|
+
*/
|
|
4066
|
+
async mdelete(keys) {
|
|
4067
|
+
return this.storage.mdelete(keys);
|
|
4068
|
+
}
|
|
4069
|
+
/**
|
|
4070
|
+
* Update TTL on an existing key.
|
|
4071
|
+
*
|
|
4072
|
+
* @param key - Storage key
|
|
4073
|
+
* @param ttlSeconds - New TTL in seconds
|
|
4074
|
+
* @returns true if key exists and TTL was set
|
|
4075
|
+
*/
|
|
4076
|
+
async expire(key, ttlSeconds) {
|
|
4077
|
+
return this.storage.expire(key, ttlSeconds);
|
|
4078
|
+
}
|
|
4079
|
+
/**
|
|
4080
|
+
* Get remaining TTL for a key.
|
|
4081
|
+
*
|
|
4082
|
+
* @param key - Storage key
|
|
4083
|
+
* @returns TTL in seconds, -1 if no TTL, or null if key doesn't exist
|
|
4084
|
+
*/
|
|
4085
|
+
async ttl(key) {
|
|
4086
|
+
return this.storage.ttl(key);
|
|
4087
|
+
}
|
|
4088
|
+
/**
|
|
4089
|
+
* List keys matching a pattern.
|
|
4090
|
+
*
|
|
4091
|
+
* @param pattern - Glob pattern (default: '*' for all keys)
|
|
4092
|
+
* @returns Array of matching keys
|
|
4093
|
+
*/
|
|
4094
|
+
async keys(pattern) {
|
|
4095
|
+
return this.storage.keys(pattern);
|
|
4096
|
+
}
|
|
4097
|
+
/**
|
|
4098
|
+
* Count keys matching a pattern.
|
|
4099
|
+
*
|
|
4100
|
+
* @param pattern - Glob pattern (default: '*' for all keys)
|
|
4101
|
+
* @returns Number of matching keys
|
|
4102
|
+
*/
|
|
4103
|
+
async count(pattern) {
|
|
4104
|
+
return this.storage.count(pattern);
|
|
4105
|
+
}
|
|
4106
|
+
/**
|
|
4107
|
+
* Re-encrypt a value with the active key.
|
|
4108
|
+
* Useful for key rotation - reads with old key, writes with new key.
|
|
4109
|
+
*
|
|
4110
|
+
* @param key - Storage key to re-encrypt
|
|
4111
|
+
* @param options - Optional TTL for the re-encrypted value
|
|
4112
|
+
* @returns true if value was re-encrypted, false if not found
|
|
4113
|
+
*/
|
|
4114
|
+
async reencrypt(key, options) {
|
|
4115
|
+
const value = await this.get(key);
|
|
4116
|
+
if (value === null) {
|
|
4117
|
+
return false;
|
|
4118
|
+
}
|
|
4119
|
+
await this.set(key, value, options);
|
|
4120
|
+
return true;
|
|
4121
|
+
}
|
|
4122
|
+
/**
|
|
4123
|
+
* Rotate the active encryption key.
|
|
4124
|
+
* New encryptions will use the new key; old keys remain for decryption.
|
|
4125
|
+
*
|
|
4126
|
+
* @param newKey - New encryption key to use for writes
|
|
4127
|
+
*/
|
|
4128
|
+
rotateKey(newKey) {
|
|
4129
|
+
if (newKey.key.length !== 32) {
|
|
4130
|
+
throw new EncryptedStorageError(`New encryption key must be 32 bytes (AES-256), got ${newKey.key.length} bytes`);
|
|
4131
|
+
}
|
|
4132
|
+
if (!this.keyMap.has(newKey.kid)) {
|
|
4133
|
+
this.keyMap.set(newKey.kid, newKey.key);
|
|
4134
|
+
}
|
|
4135
|
+
this.activeKey = newKey;
|
|
4136
|
+
}
|
|
4137
|
+
/**
|
|
4138
|
+
* Get the active key ID.
|
|
4139
|
+
*/
|
|
4140
|
+
get activeKeyId() {
|
|
4141
|
+
return this.activeKey.kid;
|
|
4142
|
+
}
|
|
4143
|
+
/**
|
|
4144
|
+
* Get all known key IDs.
|
|
4145
|
+
*/
|
|
4146
|
+
get keyIds() {
|
|
4147
|
+
return Array.from(this.keyMap.keys());
|
|
4148
|
+
}
|
|
4149
|
+
/**
|
|
4150
|
+
* Get the underlying storage adapter.
|
|
4151
|
+
* Use with caution - operations bypass encryption.
|
|
4152
|
+
*/
|
|
4153
|
+
get raw() {
|
|
4154
|
+
return this.storage;
|
|
4155
|
+
}
|
|
4156
|
+
/**
|
|
4157
|
+
* Check if client-side key binding is enabled.
|
|
4158
|
+
* When true, encryption keys are derived from both server key and client secret.
|
|
4159
|
+
*/
|
|
4160
|
+
get hasClientBinding() {
|
|
4161
|
+
return this.clientBinding !== void 0;
|
|
4162
|
+
}
|
|
4163
|
+
/**
|
|
4164
|
+
* Derive the actual encryption key.
|
|
4165
|
+
*
|
|
4166
|
+
* If clientBinding is configured, combines server key with client secret
|
|
4167
|
+
* using HKDF-SHA256 (RFC 5869) to produce the actual encryption key.
|
|
4168
|
+
* Otherwise, returns the server key as-is.
|
|
4169
|
+
*
|
|
4170
|
+
* This provides zero-knowledge encryption where:
|
|
4171
|
+
* - Server cannot decrypt without client secret (sessionId)
|
|
4172
|
+
* - Client cannot decrypt without server key
|
|
4173
|
+
* - Key derivation is deterministic (same inputs -> same derived key)
|
|
4174
|
+
*
|
|
4175
|
+
* @param serverKey - The server-side encryption key
|
|
4176
|
+
* @returns The key to use for actual encryption/decryption
|
|
4177
|
+
*/
|
|
4178
|
+
deriveKey(serverKey) {
|
|
4179
|
+
if (!this.clientBinding) {
|
|
4180
|
+
return serverKey;
|
|
4181
|
+
}
|
|
4182
|
+
const clientSecret = typeof this.clientBinding.secret === "string" ? textEncoder2.encode(this.clientBinding.secret) : this.clientBinding.secret;
|
|
4183
|
+
const ikm = new Uint8Array(serverKey.length + clientSecret.length);
|
|
4184
|
+
ikm.set(serverKey, 0);
|
|
4185
|
+
ikm.set(clientSecret, serverKey.length);
|
|
4186
|
+
const salt = this.clientBinding.salt ?? new Uint8Array(0);
|
|
4187
|
+
const info = textEncoder2.encode(this.clientBinding.info ?? "frontmcp-client-bound-v1");
|
|
4188
|
+
return hkdfSha256(ikm, salt, info, 32);
|
|
4189
|
+
}
|
|
4190
|
+
/**
|
|
4191
|
+
* Encrypt a value using the active key.
|
|
4192
|
+
* If client binding is configured, uses derived key from server key + client secret.
|
|
4193
|
+
*/
|
|
4194
|
+
encryptValue(value) {
|
|
4195
|
+
let jsonString;
|
|
4196
|
+
try {
|
|
4197
|
+
jsonString = JSON.stringify(value);
|
|
4198
|
+
} catch (error) {
|
|
4199
|
+
throw new EncryptedStorageError(`Failed to serialize value: ${error.message}`);
|
|
4200
|
+
}
|
|
4201
|
+
const plaintext = textEncoder2.encode(jsonString);
|
|
4202
|
+
const iv = randomBytes(12);
|
|
4203
|
+
const encryptionKey = this.deriveKey(this.activeKey.key);
|
|
4204
|
+
const { ciphertext, tag } = encryptAesGcm(encryptionKey, plaintext, iv);
|
|
4205
|
+
return {
|
|
4206
|
+
alg: "A256GCM",
|
|
4207
|
+
kid: this.activeKey.kid,
|
|
4208
|
+
iv: base64urlEncode(iv),
|
|
4209
|
+
tag: base64urlEncode(tag),
|
|
4210
|
+
data: base64urlEncode(ciphertext)
|
|
4211
|
+
};
|
|
4212
|
+
}
|
|
4213
|
+
/**
|
|
4214
|
+
* Decrypt and parse a stored value.
|
|
4215
|
+
*/
|
|
4216
|
+
decryptAndParse(raw, storageKey) {
|
|
4217
|
+
let blob;
|
|
4218
|
+
try {
|
|
4219
|
+
blob = JSON.parse(raw);
|
|
4220
|
+
} catch (_error) {
|
|
4221
|
+
if (this.throwOnError) {
|
|
4222
|
+
throw new EncryptedStorageError(`Failed to parse stored blob for key "${storageKey}"`);
|
|
4223
|
+
}
|
|
4224
|
+
return null;
|
|
4225
|
+
}
|
|
4226
|
+
if (!this.isValidBlob(blob)) {
|
|
4227
|
+
if (this.throwOnError) {
|
|
4228
|
+
throw new EncryptedStorageError(`Invalid encrypted blob structure for key "${storageKey}"`);
|
|
4229
|
+
}
|
|
4230
|
+
return null;
|
|
4231
|
+
}
|
|
4232
|
+
const serverKey = this.keyMap.get(blob.kid);
|
|
4233
|
+
if (!serverKey) {
|
|
4234
|
+
if (this.throwOnError) {
|
|
4235
|
+
throw new EncryptedStorageError(
|
|
4236
|
+
`Unknown encryption key "${blob.kid}" for key "${storageKey}". Known keys: ${Array.from(this.keyMap.keys()).join(", ")}`
|
|
4237
|
+
);
|
|
4238
|
+
}
|
|
4239
|
+
return null;
|
|
4240
|
+
}
|
|
4241
|
+
const decryptionKey = this.deriveKey(serverKey);
|
|
4242
|
+
let decrypted;
|
|
4243
|
+
try {
|
|
4244
|
+
const iv = base64urlDecode(blob.iv);
|
|
4245
|
+
const tag = base64urlDecode(blob.tag);
|
|
4246
|
+
const ciphertext = base64urlDecode(blob.data);
|
|
4247
|
+
const plaintext = decryptAesGcm(decryptionKey, ciphertext, iv, tag);
|
|
4248
|
+
decrypted = JSON.parse(textDecoder2.decode(plaintext));
|
|
4249
|
+
} catch (error) {
|
|
4250
|
+
if (this.throwOnError) {
|
|
4251
|
+
throw new EncryptedStorageError(`Decryption failed for key "${storageKey}": ${error.message}`);
|
|
4252
|
+
}
|
|
4253
|
+
return null;
|
|
4254
|
+
}
|
|
4255
|
+
if (this.schema) {
|
|
4256
|
+
const result = this.schema.safeParse(decrypted);
|
|
4257
|
+
if (!result.success) {
|
|
4258
|
+
if (this.throwOnError) {
|
|
4259
|
+
throw new EncryptedStorageError(`Schema validation failed for key "${storageKey}": ${result.error.message}`);
|
|
4260
|
+
}
|
|
4261
|
+
return null;
|
|
4262
|
+
}
|
|
4263
|
+
decrypted = result.data;
|
|
4264
|
+
}
|
|
4265
|
+
if (blob.kid !== this.activeKey.kid && this.onKeyRotationNeeded) {
|
|
4266
|
+
this.onKeyRotationNeeded(storageKey, blob.kid, this.activeKey.kid);
|
|
4267
|
+
}
|
|
4268
|
+
return decrypted;
|
|
4269
|
+
}
|
|
4270
|
+
/**
|
|
4271
|
+
* Validate blob structure.
|
|
4272
|
+
*/
|
|
4273
|
+
isValidBlob(blob) {
|
|
4274
|
+
return typeof blob === "object" && blob !== null && "alg" in blob && "kid" in blob && "iv" in blob && "tag" in blob && "data" in blob && blob.alg === "A256GCM" && typeof blob.kid === "string" && typeof blob.iv === "string" && typeof blob.tag === "string" && typeof blob.data === "string";
|
|
4275
|
+
}
|
|
4276
|
+
};
|
|
4277
|
+
|
|
3106
4278
|
// libs/utils/src/storage/adapters/index.ts
|
|
3107
4279
|
init_base();
|
|
3108
4280
|
init_redis();
|
|
@@ -3110,12 +4282,17 @@ init_vercel_kv();
|
|
|
3110
4282
|
init_upstash();
|
|
3111
4283
|
|
|
3112
4284
|
// libs/utils/src/storage/index.ts
|
|
4285
|
+
init_pattern();
|
|
3113
4286
|
init_ttl();
|
|
3114
4287
|
export {
|
|
3115
4288
|
BaseStorageAdapter,
|
|
3116
4289
|
DEFAULT_CODE_VERIFIER_LENGTH,
|
|
3117
4290
|
DEFAULT_MAX_INPUT_LENGTH,
|
|
3118
4291
|
EncryptedBlobError,
|
|
4292
|
+
EncryptedStorageError,
|
|
4293
|
+
EncryptedTypedStorage,
|
|
4294
|
+
FileSystemStorageAdapter,
|
|
4295
|
+
KeyPersistence,
|
|
3119
4296
|
MAX_CODE_VERIFIER_LENGTH,
|
|
3120
4297
|
MAX_TTL_SECONDS,
|
|
3121
4298
|
MIN_CODE_VERIFIER_LENGTH,
|
|
@@ -3133,11 +4310,17 @@ export {
|
|
|
3133
4310
|
StorageOperationError,
|
|
3134
4311
|
StoragePatternError,
|
|
3135
4312
|
StorageTTLError,
|
|
4313
|
+
TypedStorage,
|
|
3136
4314
|
UpstashStorageAdapter,
|
|
3137
4315
|
VercelKvStorageAdapter,
|
|
3138
4316
|
access,
|
|
3139
4317
|
analyzePattern,
|
|
4318
|
+
anyKeyDataSchema,
|
|
3140
4319
|
assertNode,
|
|
4320
|
+
asymmetricAlgSchema,
|
|
4321
|
+
asymmetricKeyDataSchema,
|
|
4322
|
+
base64Decode,
|
|
4323
|
+
base64Encode,
|
|
3141
4324
|
base64urlDecode,
|
|
3142
4325
|
base64urlEncode,
|
|
3143
4326
|
buildPrefix,
|
|
@@ -3147,6 +4330,8 @@ export {
|
|
|
3147
4330
|
collapseWhitespace,
|
|
3148
4331
|
copyFile,
|
|
3149
4332
|
cp,
|
|
4333
|
+
createKeyPersistence,
|
|
4334
|
+
createKeyPersistenceWithStorage,
|
|
3150
4335
|
createMemoryStorage,
|
|
3151
4336
|
createNamespacedStorage,
|
|
3152
4337
|
createRootStorage,
|
|
@@ -3188,6 +4373,7 @@ export {
|
|
|
3188
4373
|
hmacSha256,
|
|
3189
4374
|
idFromString,
|
|
3190
4375
|
inferMimeType,
|
|
4376
|
+
isAsymmetricKeyData,
|
|
3191
4377
|
isBrowser,
|
|
3192
4378
|
isDirEmpty,
|
|
3193
4379
|
isExpired,
|
|
@@ -3196,7 +4382,9 @@ export {
|
|
|
3196
4382
|
isPatternSafe,
|
|
3197
4383
|
isRsaPssAlg,
|
|
3198
4384
|
isSecretCached,
|
|
4385
|
+
isSecretKeyData,
|
|
3199
4386
|
isSecretPersistenceEnabled,
|
|
4387
|
+
isSignedData,
|
|
3200
4388
|
isUriTemplate,
|
|
3201
4389
|
isValidCodeChallenge,
|
|
3202
4390
|
isValidCodeVerifier,
|
|
@@ -3211,12 +4399,14 @@ export {
|
|
|
3211
4399
|
mkdir,
|
|
3212
4400
|
mkdtemp,
|
|
3213
4401
|
normalizeTTL,
|
|
4402
|
+
parseKeyData,
|
|
3214
4403
|
parseSecretData,
|
|
3215
4404
|
parseUriTemplate,
|
|
3216
4405
|
randomBytes,
|
|
3217
4406
|
randomUUID,
|
|
3218
4407
|
readFile,
|
|
3219
4408
|
readFileBuffer,
|
|
4409
|
+
readFileSync,
|
|
3220
4410
|
readJSON,
|
|
3221
4411
|
readdir,
|
|
3222
4412
|
rename,
|
|
@@ -3233,12 +4423,14 @@ export {
|
|
|
3233
4423
|
sanitizeToJson,
|
|
3234
4424
|
saveSecret,
|
|
3235
4425
|
secretDataSchema,
|
|
4426
|
+
secretKeyDataSchema,
|
|
3236
4427
|
sepFor,
|
|
3237
4428
|
serializeBlob,
|
|
3238
4429
|
sha256,
|
|
3239
4430
|
sha256Base64url,
|
|
3240
4431
|
sha256Hex,
|
|
3241
4432
|
shortHash,
|
|
4433
|
+
signData,
|
|
3242
4434
|
splitWords,
|
|
3243
4435
|
stat,
|
|
3244
4436
|
timingSafeEqual,
|
|
@@ -3254,11 +4446,14 @@ export {
|
|
|
3254
4446
|
ttlToExpiresAt,
|
|
3255
4447
|
unlink,
|
|
3256
4448
|
validateBaseUrl,
|
|
4449
|
+
validateKeyData,
|
|
3257
4450
|
validateOptionalTTL,
|
|
3258
4451
|
validatePattern,
|
|
3259
4452
|
validateSecretData,
|
|
3260
4453
|
validateTTL,
|
|
3261
4454
|
verifyCodeChallenge,
|
|
4455
|
+
verifyData,
|
|
4456
|
+
verifyOrParseData,
|
|
3262
4457
|
writeFile,
|
|
3263
4458
|
writeJSON
|
|
3264
4459
|
};
|