@frontmcp/utils 1.10.16 → 1.11.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/browser/async-context/browser-async-context.mjs +177 -0
- package/browser/async-context/index.mjs +15 -0
- package/browser/async-context/overlap-error.mjs +10 -0
- package/browser/class-brand/class-brand.mjs +27 -0
- package/browser/class-brand/index.mjs +6 -0
- package/browser/content/content.mjs +151 -0
- package/browser/content/index.mjs +8 -0
- package/browser/crypto/browser.mjs +103 -0
- package/browser/crypto/encrypted-blob.mjs +119 -0
- package/browser/crypto/hmac-signing.mjs +70 -0
- package/browser/crypto/index.mjs +281 -0
- package/browser/crypto/jwt-alg.mjs +40 -0
- package/browser/crypto/key-persistence/factory.mjs +134 -0
- package/browser/crypto/key-persistence/index.mjs +26 -0
- package/browser/crypto/key-persistence/key-persistence.mjs +185 -0
- package/browser/crypto/key-persistence/schemas.mjs +82 -0
- package/browser/crypto/pkce/index.mjs +25 -0
- package/browser/crypto/pkce/pkce.mjs +59 -0
- package/browser/crypto/runtime.mjs +17 -0
- package/browser/crypto/secret-persistence/index.mjs +29 -0
- package/browser/crypto/secret-persistence/persistence.mjs +181 -0
- package/browser/crypto/secret-persistence/schema.mjs +40 -0
- package/browser/crypto/web-rsa.mjs +36 -0
- package/browser/env/browser-env.mjs +56 -0
- package/browser/env/browser-runtime-context.mjs +85 -0
- package/browser/env/build-target.mjs +25 -0
- package/browser/env/edge-scope.mjs +17 -0
- package/browser/env/index.mjs +47 -0
- package/browser/env/provider.mjs +46 -0
- package/browser/escape/escape.mjs +43 -0
- package/browser/escape/index.mjs +9 -0
- package/browser/event-emitter/browser-event-emitter.mjs +49 -0
- package/browser/event-emitter/index.mjs +5 -0
- package/browser/fs/fs.mjs +227 -0
- package/browser/fs/index.mjs +65 -0
- package/browser/http/cookie.mjs +85 -0
- package/browser/http/http.mjs +18 -0
- package/browser/http/index.mjs +11 -0
- package/browser/index.mjs +526 -0
- package/browser/llm/index.mjs +5 -0
- package/browser/llm/llm-tool-handlers.mjs +55 -0
- package/browser/machine-id/index.mjs +7 -0
- package/browser/machine-id/machine-id.mjs +108 -0
- package/browser/naming/index.mjs +10 -0
- package/browser/naming/naming.mjs +68 -0
- package/browser/net/index.mjs +10 -0
- package/browser/net/ip-address.mjs +128 -0
- package/browser/net/redirect.mjs +7 -0
- package/browser/path/browser-path.mjs +47 -0
- package/browser/path/index.mjs +24 -0
- package/browser/path/path.mjs +13 -0
- package/browser/process/child-process.mjs +23 -0
- package/browser/process/index.mjs +15 -0
- package/browser/process/owned-process.mjs +175 -0
- package/browser/process/process-tree.mjs +149 -0
- package/browser/process/process-watch.mjs +48 -0
- package/browser/redaction/index.mjs +6 -0
- package/browser/redaction/redaction.mjs +74 -0
- package/browser/regex/index.mjs +3 -0
- package/browser/regex/patterns.mjs +167 -0
- package/browser/regex/safe-regex.mjs +93 -0
- package/browser/serialization/index.mjs +5 -0
- package/browser/serialization/serialization.mjs +22 -0
- package/browser/stdio-bridge/errors.mjs +27 -0
- package/browser/stdio-bridge/index.mjs +24 -0
- package/browser/stdio-bridge/list-changed.mjs +21 -0
- package/browser/stdio-bridge/state-machine.mjs +211 -0
- package/browser/stdio-bridge/stdio-framer.mjs +91 -0
- package/browser/stdio-bridge/upstream-client.mjs +189 -0
- package/browser/storage/adapters/base.mjs +139 -0
- package/browser/storage/adapters/cloudflare-kv.mjs +149 -0
- package/browser/storage/adapters/filesystem.mjs +279 -0
- package/browser/storage/adapters/index.mjs +23 -0
- package/browser/storage/adapters/indexeddb.mjs +198 -0
- package/browser/storage/adapters/localstorage.mjs +222 -0
- package/browser/storage/adapters/memory.mjs +350 -0
- package/browser/storage/adapters/redis.mjs +497 -0
- package/browser/storage/adapters/upstash.mjs +299 -0
- package/browser/storage/adapters/vercel-kv.mjs +226 -0
- package/browser/storage/change-feed.mjs +189 -0
- package/browser/storage/encrypted-typed-storage.mjs +329 -0
- package/browser/storage/errors.mjs +92 -0
- package/browser/storage/factory.mjs +155 -0
- package/browser/storage/index.mjs +96 -0
- package/browser/storage/namespace.mjs +186 -0
- package/browser/storage/redis-error-listener.mjs +32 -0
- package/browser/storage/redis-url.mjs +66 -0
- package/browser/storage/typed-storage.mjs +171 -0
- package/browser/storage/utils/compare-and-delete.mjs +9 -0
- package/browser/storage/utils/index.mjs +24 -0
- package/browser/storage/utils/pattern.mjs +68 -0
- package/browser/storage/utils/ttl.mjs +48 -0
- package/browser/uri/index.mjs +19 -0
- package/browser/uri/uri-template.mjs +86 -0
- package/browser/uri/uri-validation.mjs +18 -0
- package/crypto/browser.d.ts +1 -13
- package/crypto/browser.js +1 -1
- package/crypto/node.js +1 -1
- package/crypto/web-rsa.d.ts +13 -0
- package/env/browser-runtime-context.js +1 -1
- package/env/runtime-context.js +1 -1
- package/esm/crypto/browser.mjs +1 -1
- package/esm/crypto/node.mjs +1 -1
- package/esm/env/browser-runtime-context.mjs +1 -1
- package/esm/env/runtime-context.mjs +1 -1
- package/esm/index.mjs +1 -1
- package/fs/fs.d.ts +20 -4
- package/fs/index.d.ts +1 -1
- package/index.d.ts +9 -3
- package/index.js +1 -1
- package/package.json +24 -8
- package/process/child-process.d.ts +2 -0
- package/process/index.d.ts +6 -0
- package/process/owned-process.d.ts +59 -0
- package/process/process-tree.d.ts +78 -0
- package/process/process-watch.d.ts +41 -0
- package/redaction/index.d.ts +2 -0
- package/redaction/redaction.d.ts +24 -0
- package/stdio-bridge/errors.d.ts +23 -0
- package/stdio-bridge/index.d.ts +9 -0
- package/stdio-bridge/list-changed.d.ts +3 -0
- package/stdio-bridge/state-machine.d.ts +61 -0
- package/stdio-bridge/stdio-framer.d.ts +49 -0
- package/stdio-bridge/types.d.ts +10 -0
- package/stdio-bridge/upstream-client.d.ts +66 -0
- package/storage/adapters/base.d.ts +2 -0
- package/storage/adapters/memory.d.ts +1 -0
- package/storage/adapters/redis.d.ts +3 -0
- package/storage/change-feed.d.ts +47 -0
- package/storage/factory.d.ts +6 -0
- package/storage/index.d.ts +3 -1
- package/storage/namespace.d.ts +2 -0
- package/storage/types.d.ts +46 -1
- package/storage/utils/compare-and-delete.d.ts +2 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// libs/utils/src/storage/encrypted-typed-storage.ts
|
|
2
|
+
import { encryptAesGcm, decryptAesGcm, randomBytes, base64urlEncode, base64urlDecode, hkdfSha256 } from "../crypto/index.mjs";
|
|
3
|
+
import { EncryptedStorageError } from "./errors.mjs";
|
|
4
|
+
var textEncoder = new TextEncoder();
|
|
5
|
+
var textDecoder = new TextDecoder();
|
|
6
|
+
var EncryptedTypedStorage = class {
|
|
7
|
+
storage;
|
|
8
|
+
activeKey;
|
|
9
|
+
keyMap;
|
|
10
|
+
schema;
|
|
11
|
+
throwOnError;
|
|
12
|
+
onKeyRotationNeeded;
|
|
13
|
+
clientBinding;
|
|
14
|
+
constructor(storage, options) {
|
|
15
|
+
if (!options.keys || options.keys.length === 0) {
|
|
16
|
+
throw new EncryptedStorageError("At least one encryption key is required");
|
|
17
|
+
}
|
|
18
|
+
for (const k of options.keys) {
|
|
19
|
+
if (k.key.length !== 32) {
|
|
20
|
+
throw new EncryptedStorageError(
|
|
21
|
+
`Encryption key "${k.kid}" must be 32 bytes (AES-256), got ${k.key.length} bytes`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
this.storage = storage;
|
|
26
|
+
this.activeKey = options.keys[0];
|
|
27
|
+
this.keyMap = new Map(options.keys.map((k) => [k.kid, k.key]));
|
|
28
|
+
this.schema = options.schema;
|
|
29
|
+
this.throwOnError = options.throwOnError ?? false;
|
|
30
|
+
this.onKeyRotationNeeded = options.onKeyRotationNeeded;
|
|
31
|
+
this.clientBinding = options.clientBinding;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get a decrypted value by key.
|
|
35
|
+
*
|
|
36
|
+
* @param key - Storage key
|
|
37
|
+
* @returns The decrypted value, or null if not found or decryption fails
|
|
38
|
+
*/
|
|
39
|
+
async get(key) {
|
|
40
|
+
const raw = await this.storage.get(key);
|
|
41
|
+
if (raw === null) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return this.decryptAndParse(raw, key);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Encrypt and store a value.
|
|
48
|
+
*
|
|
49
|
+
* @param key - Storage key
|
|
50
|
+
* @param value - Value to encrypt and store
|
|
51
|
+
* @param options - Optional TTL and conditional flags
|
|
52
|
+
*/
|
|
53
|
+
async set(key, value, options) {
|
|
54
|
+
const encrypted = this.encryptValue(value);
|
|
55
|
+
const serialized = JSON.stringify(encrypted);
|
|
56
|
+
await this.storage.set(key, serialized, options);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Delete a key.
|
|
60
|
+
*
|
|
61
|
+
* @param key - Storage key
|
|
62
|
+
* @returns true if key existed and was deleted
|
|
63
|
+
*/
|
|
64
|
+
async delete(key) {
|
|
65
|
+
return this.storage.delete(key);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if a key exists.
|
|
69
|
+
*
|
|
70
|
+
* @param key - Storage key
|
|
71
|
+
* @returns true if key exists
|
|
72
|
+
*/
|
|
73
|
+
async exists(key) {
|
|
74
|
+
return this.storage.exists(key);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get multiple decrypted values.
|
|
78
|
+
*
|
|
79
|
+
* @param keys - Array of storage keys
|
|
80
|
+
* @returns Array of decrypted values (null for missing/invalid keys)
|
|
81
|
+
*/
|
|
82
|
+
async mget(keys) {
|
|
83
|
+
if (keys.length === 0) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const rawValues = await this.storage.mget(keys);
|
|
87
|
+
return rawValues.map((raw, index) => {
|
|
88
|
+
if (raw === null) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return this.decryptAndParse(raw, keys[index]);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Encrypt and store multiple values.
|
|
96
|
+
*
|
|
97
|
+
* @param entries - Array of key-value-options entries
|
|
98
|
+
*/
|
|
99
|
+
async mset(entries) {
|
|
100
|
+
if (entries.length === 0) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const rawEntries = entries.map((entry) => ({
|
|
104
|
+
key: entry.key,
|
|
105
|
+
value: JSON.stringify(this.encryptValue(entry.value)),
|
|
106
|
+
options: entry.options
|
|
107
|
+
}));
|
|
108
|
+
await this.storage.mset(rawEntries);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Delete multiple keys.
|
|
112
|
+
*
|
|
113
|
+
* @param keys - Array of storage keys
|
|
114
|
+
* @returns Number of keys actually deleted
|
|
115
|
+
*/
|
|
116
|
+
async mdelete(keys) {
|
|
117
|
+
return this.storage.mdelete(keys);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Update TTL on an existing key.
|
|
121
|
+
*
|
|
122
|
+
* @param key - Storage key
|
|
123
|
+
* @param ttlSeconds - New TTL in seconds
|
|
124
|
+
* @returns true if key exists and TTL was set
|
|
125
|
+
*/
|
|
126
|
+
async expire(key, ttlSeconds) {
|
|
127
|
+
return this.storage.expire(key, ttlSeconds);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get remaining TTL for a key.
|
|
131
|
+
*
|
|
132
|
+
* @param key - Storage key
|
|
133
|
+
* @returns TTL in seconds, -1 if no TTL, or null if key doesn't exist
|
|
134
|
+
*/
|
|
135
|
+
async ttl(key) {
|
|
136
|
+
return this.storage.ttl(key);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* List keys matching a pattern.
|
|
140
|
+
*
|
|
141
|
+
* @param pattern - Glob pattern (default: '*' for all keys)
|
|
142
|
+
* @returns Array of matching keys
|
|
143
|
+
*/
|
|
144
|
+
async keys(pattern) {
|
|
145
|
+
return this.storage.keys(pattern);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Count keys matching a pattern.
|
|
149
|
+
*
|
|
150
|
+
* @param pattern - Glob pattern (default: '*' for all keys)
|
|
151
|
+
* @returns Number of matching keys
|
|
152
|
+
*/
|
|
153
|
+
async count(pattern) {
|
|
154
|
+
return this.storage.count(pattern);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Re-encrypt a value with the active key.
|
|
158
|
+
* Useful for key rotation - reads with old key, writes with new key.
|
|
159
|
+
*
|
|
160
|
+
* @param key - Storage key to re-encrypt
|
|
161
|
+
* @param options - Optional TTL for the re-encrypted value
|
|
162
|
+
* @returns true if value was re-encrypted, false if not found
|
|
163
|
+
*/
|
|
164
|
+
async reencrypt(key, options) {
|
|
165
|
+
const value = await this.get(key);
|
|
166
|
+
if (value === null) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
await this.set(key, value, options);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Rotate the active encryption key.
|
|
174
|
+
* New encryptions will use the new key; old keys remain for decryption.
|
|
175
|
+
*
|
|
176
|
+
* @param newKey - New encryption key to use for writes
|
|
177
|
+
*/
|
|
178
|
+
rotateKey(newKey) {
|
|
179
|
+
if (newKey.key.length !== 32) {
|
|
180
|
+
throw new EncryptedStorageError(`New encryption key must be 32 bytes (AES-256), got ${newKey.key.length} bytes`);
|
|
181
|
+
}
|
|
182
|
+
if (!this.keyMap.has(newKey.kid)) {
|
|
183
|
+
this.keyMap.set(newKey.kid, newKey.key);
|
|
184
|
+
}
|
|
185
|
+
this.activeKey = newKey;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get the active key ID.
|
|
189
|
+
*/
|
|
190
|
+
get activeKeyId() {
|
|
191
|
+
return this.activeKey.kid;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get all known key IDs.
|
|
195
|
+
*/
|
|
196
|
+
get keyIds() {
|
|
197
|
+
return Array.from(this.keyMap.keys());
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get the underlying storage adapter.
|
|
201
|
+
* Use with caution - operations bypass encryption.
|
|
202
|
+
*/
|
|
203
|
+
get raw() {
|
|
204
|
+
return this.storage;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Check if client-side key binding is enabled.
|
|
208
|
+
* When true, encryption keys are derived from both server key and client secret.
|
|
209
|
+
*/
|
|
210
|
+
get hasClientBinding() {
|
|
211
|
+
return this.clientBinding !== void 0;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Derive the actual encryption key.
|
|
215
|
+
*
|
|
216
|
+
* If clientBinding is configured, combines server key with client secret
|
|
217
|
+
* using HKDF-SHA256 (RFC 5869) to produce the actual encryption key.
|
|
218
|
+
* Otherwise, returns the server key as-is.
|
|
219
|
+
*
|
|
220
|
+
* This provides zero-knowledge encryption where:
|
|
221
|
+
* - Server cannot decrypt without client secret (sessionId)
|
|
222
|
+
* - Client cannot decrypt without server key
|
|
223
|
+
* - Key derivation is deterministic (same inputs -> same derived key)
|
|
224
|
+
*
|
|
225
|
+
* @param serverKey - The server-side encryption key
|
|
226
|
+
* @returns The key to use for actual encryption/decryption
|
|
227
|
+
*/
|
|
228
|
+
deriveKey(serverKey) {
|
|
229
|
+
if (!this.clientBinding) {
|
|
230
|
+
return serverKey;
|
|
231
|
+
}
|
|
232
|
+
const clientSecret = typeof this.clientBinding.secret === "string" ? textEncoder.encode(this.clientBinding.secret) : this.clientBinding.secret;
|
|
233
|
+
const ikm = new Uint8Array(serverKey.length + clientSecret.length);
|
|
234
|
+
ikm.set(serverKey, 0);
|
|
235
|
+
ikm.set(clientSecret, serverKey.length);
|
|
236
|
+
const salt = this.clientBinding.salt ?? new Uint8Array(0);
|
|
237
|
+
const info = textEncoder.encode(this.clientBinding.info ?? "frontmcp-client-bound-v1");
|
|
238
|
+
return hkdfSha256(ikm, salt, info, 32);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Encrypt a value using the active key.
|
|
242
|
+
* If client binding is configured, uses derived key from server key + client secret.
|
|
243
|
+
*/
|
|
244
|
+
encryptValue(value) {
|
|
245
|
+
let jsonString;
|
|
246
|
+
try {
|
|
247
|
+
jsonString = JSON.stringify(value);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
throw new EncryptedStorageError(`Failed to serialize value: ${error.message}`);
|
|
250
|
+
}
|
|
251
|
+
const plaintext = textEncoder.encode(jsonString);
|
|
252
|
+
const iv = randomBytes(12);
|
|
253
|
+
const encryptionKey = this.deriveKey(this.activeKey.key);
|
|
254
|
+
const { ciphertext, tag } = encryptAesGcm(encryptionKey, plaintext, iv);
|
|
255
|
+
return {
|
|
256
|
+
alg: "A256GCM",
|
|
257
|
+
kid: this.activeKey.kid,
|
|
258
|
+
iv: base64urlEncode(iv),
|
|
259
|
+
tag: base64urlEncode(tag),
|
|
260
|
+
data: base64urlEncode(ciphertext)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Decrypt and parse a stored value.
|
|
265
|
+
*/
|
|
266
|
+
decryptAndParse(raw, storageKey) {
|
|
267
|
+
let blob;
|
|
268
|
+
try {
|
|
269
|
+
blob = JSON.parse(raw);
|
|
270
|
+
} catch (_error) {
|
|
271
|
+
if (this.throwOnError) {
|
|
272
|
+
throw new EncryptedStorageError(`Failed to parse stored blob for key "${storageKey}"`);
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
if (!this.isValidBlob(blob)) {
|
|
277
|
+
if (this.throwOnError) {
|
|
278
|
+
throw new EncryptedStorageError(`Invalid encrypted blob structure for key "${storageKey}"`);
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const serverKey = this.keyMap.get(blob.kid);
|
|
283
|
+
if (!serverKey) {
|
|
284
|
+
if (this.throwOnError) {
|
|
285
|
+
throw new EncryptedStorageError(
|
|
286
|
+
`Unknown encryption key "${blob.kid}" for key "${storageKey}". Known keys: ${Array.from(this.keyMap.keys()).join(", ")}`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
const decryptionKey = this.deriveKey(serverKey);
|
|
292
|
+
let decrypted;
|
|
293
|
+
try {
|
|
294
|
+
const iv = base64urlDecode(blob.iv);
|
|
295
|
+
const tag = base64urlDecode(blob.tag);
|
|
296
|
+
const ciphertext = base64urlDecode(blob.data);
|
|
297
|
+
const plaintext = decryptAesGcm(decryptionKey, ciphertext, iv, tag);
|
|
298
|
+
decrypted = JSON.parse(textDecoder.decode(plaintext));
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (this.throwOnError) {
|
|
301
|
+
throw new EncryptedStorageError(`Decryption failed for key "${storageKey}": ${error.message}`);
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
if (this.schema) {
|
|
306
|
+
const result = this.schema.safeParse(decrypted);
|
|
307
|
+
if (!result.success) {
|
|
308
|
+
if (this.throwOnError) {
|
|
309
|
+
throw new EncryptedStorageError(`Schema validation failed for key "${storageKey}": ${result.error.message}`);
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
decrypted = result.data;
|
|
314
|
+
}
|
|
315
|
+
if (blob.kid !== this.activeKey.kid && this.onKeyRotationNeeded) {
|
|
316
|
+
this.onKeyRotationNeeded(storageKey, blob.kid, this.activeKey.kid);
|
|
317
|
+
}
|
|
318
|
+
return decrypted;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Validate blob structure.
|
|
322
|
+
*/
|
|
323
|
+
isValidBlob(blob) {
|
|
324
|
+
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";
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
export {
|
|
328
|
+
EncryptedTypedStorage
|
|
329
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// libs/utils/src/storage/errors.ts
|
|
2
|
+
var StorageError = class extends Error {
|
|
3
|
+
cause;
|
|
4
|
+
constructor(message, cause) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.cause = cause;
|
|
7
|
+
this.name = "StorageError";
|
|
8
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
9
|
+
if (Error.captureStackTrace) {
|
|
10
|
+
Error.captureStackTrace(this, this.constructor);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var StorageConnectionError = class extends StorageError {
|
|
15
|
+
constructor(message, cause, backend) {
|
|
16
|
+
const fullMessage = cause ? `${message}: ${cause.message}` : message;
|
|
17
|
+
super(fullMessage, cause);
|
|
18
|
+
this.backend = backend;
|
|
19
|
+
this.name = "StorageConnectionError";
|
|
20
|
+
}
|
|
21
|
+
backend;
|
|
22
|
+
};
|
|
23
|
+
var StorageOperationError = class extends StorageError {
|
|
24
|
+
constructor(operation, key, message, cause) {
|
|
25
|
+
super(`${operation} failed for key "${key}": ${message}`, cause);
|
|
26
|
+
this.operation = operation;
|
|
27
|
+
this.key = key;
|
|
28
|
+
this.name = "StorageOperationError";
|
|
29
|
+
}
|
|
30
|
+
operation;
|
|
31
|
+
key;
|
|
32
|
+
};
|
|
33
|
+
var StorageNotSupportedError = class extends StorageError {
|
|
34
|
+
constructor(operation, backend, suggestion) {
|
|
35
|
+
const msg = suggestion ? `${operation} is not supported by ${backend}. ${suggestion}` : `${operation} is not supported by ${backend}`;
|
|
36
|
+
super(msg);
|
|
37
|
+
this.operation = operation;
|
|
38
|
+
this.backend = backend;
|
|
39
|
+
this.name = "StorageNotSupportedError";
|
|
40
|
+
}
|
|
41
|
+
operation;
|
|
42
|
+
backend;
|
|
43
|
+
};
|
|
44
|
+
var StorageConfigError = class extends StorageError {
|
|
45
|
+
constructor(backend, message) {
|
|
46
|
+
super(`Invalid ${backend} configuration: ${message}`);
|
|
47
|
+
this.backend = backend;
|
|
48
|
+
this.name = "StorageConfigError";
|
|
49
|
+
}
|
|
50
|
+
backend;
|
|
51
|
+
};
|
|
52
|
+
var StorageTTLError = class extends StorageError {
|
|
53
|
+
constructor(ttl, message) {
|
|
54
|
+
super(message ?? `Invalid TTL value: ${ttl}. TTL must be a positive integer.`);
|
|
55
|
+
this.ttl = ttl;
|
|
56
|
+
this.name = "StorageTTLError";
|
|
57
|
+
}
|
|
58
|
+
ttl;
|
|
59
|
+
};
|
|
60
|
+
var StoragePatternError = class extends StorageError {
|
|
61
|
+
constructor(pattern, message) {
|
|
62
|
+
super(`Invalid pattern "${pattern}": ${message}`);
|
|
63
|
+
this.pattern = pattern;
|
|
64
|
+
this.name = "StoragePatternError";
|
|
65
|
+
}
|
|
66
|
+
pattern;
|
|
67
|
+
};
|
|
68
|
+
var StorageNotConnectedError = class extends StorageError {
|
|
69
|
+
constructor(backend) {
|
|
70
|
+
super(`Storage backend "${backend}" is not connected. Call connect() first.`);
|
|
71
|
+
this.backend = backend;
|
|
72
|
+
this.name = "StorageNotConnectedError";
|
|
73
|
+
}
|
|
74
|
+
backend;
|
|
75
|
+
};
|
|
76
|
+
var EncryptedStorageError = class extends StorageError {
|
|
77
|
+
constructor(message) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = "EncryptedStorageError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
export {
|
|
83
|
+
EncryptedStorageError,
|
|
84
|
+
StorageConfigError,
|
|
85
|
+
StorageConnectionError,
|
|
86
|
+
StorageError,
|
|
87
|
+
StorageNotConnectedError,
|
|
88
|
+
StorageNotSupportedError,
|
|
89
|
+
StorageOperationError,
|
|
90
|
+
StoragePatternError,
|
|
91
|
+
StorageTTLError
|
|
92
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
|
|
2
|
+
const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
|
|
3
|
+
// __frontmcpCreateRequire
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// libs/utils/src/storage/factory.ts
|
|
12
|
+
import { z } from "@frontmcp/lazy-zod";
|
|
13
|
+
import { getEnv, isProduction } from "../env/browser-env.mjs";
|
|
14
|
+
import { getRuntimeContext } from "../env/browser-runtime-context.mjs";
|
|
15
|
+
import { MemoryStorageAdapter } from "./adapters/memory.mjs";
|
|
16
|
+
import { StorageConfigError } from "./errors.mjs";
|
|
17
|
+
import { createNamespacedStorage, createRootStorage } from "./namespace.mjs";
|
|
18
|
+
var SQLITE_STORAGE_PACKAGE = "@frontmcp/storage-sqlite";
|
|
19
|
+
var PATH_REQUIRED = { error: "is required (the path of the database file)" };
|
|
20
|
+
var sqliteAdapterOptionsSchema = z.object({
|
|
21
|
+
path: z.string(PATH_REQUIRED).min(1, PATH_REQUIRED),
|
|
22
|
+
walMode: z.boolean().optional(),
|
|
23
|
+
busyTimeoutMs: z.number().int().nonnegative().optional(),
|
|
24
|
+
ttlCleanupIntervalMs: z.number().int().nonnegative().optional(),
|
|
25
|
+
encryption: z.object({ secret: z.string().min(1) }).optional()
|
|
26
|
+
});
|
|
27
|
+
function parseSqliteOptions(options) {
|
|
28
|
+
const result = sqliteAdapterOptionsSchema.safeParse(options ?? {});
|
|
29
|
+
if (result.success) return result.data;
|
|
30
|
+
const problems = result.error.issues.map((issue) => `sqlite.${issue.path.join(".")}: ${issue.message}`);
|
|
31
|
+
throw new StorageConfigError("sqlite", problems.join("; "));
|
|
32
|
+
}
|
|
33
|
+
function loadSqliteStorage() {
|
|
34
|
+
try {
|
|
35
|
+
return __require(SQLITE_STORAGE_PACKAGE);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const missing = error.code === "MODULE_NOT_FOUND";
|
|
38
|
+
if (!missing || !String(error.message).includes(SQLITE_STORAGE_PACKAGE)) throw error;
|
|
39
|
+
throw new StorageConfigError(
|
|
40
|
+
"sqlite",
|
|
41
|
+
`the sqlite type needs ${SQLITE_STORAGE_PACKAGE}. Install it with: npm install ${SQLITE_STORAGE_PACKAGE}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
var noDistributedStorageWarned = false;
|
|
46
|
+
function warnNoDistributedStorage() {
|
|
47
|
+
if (noDistributedStorageWarned || getRuntimeContext().runtime === "browser") return;
|
|
48
|
+
noDistributedStorageWarned = true;
|
|
49
|
+
console.warn(
|
|
50
|
+
"[storage] Warning: No distributed storage backend detected in production. Using in-memory storage. Set REDIS_URL, UPSTASH_REDIS_REST_URL, or KV_REST_API_URL."
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
function detectStorageType() {
|
|
54
|
+
if (getEnv("UPSTASH_REDIS_REST_URL") && getEnv("UPSTASH_REDIS_REST_TOKEN")) {
|
|
55
|
+
return "upstash";
|
|
56
|
+
}
|
|
57
|
+
if (getEnv("KV_REST_API_URL") && getEnv("KV_REST_API_TOKEN")) {
|
|
58
|
+
return "vercel-kv";
|
|
59
|
+
}
|
|
60
|
+
if (getEnv("REDIS_URL") || getEnv("REDIS_HOST")) {
|
|
61
|
+
return "redis";
|
|
62
|
+
}
|
|
63
|
+
return "memory";
|
|
64
|
+
}
|
|
65
|
+
async function createAdapter(type, config) {
|
|
66
|
+
switch (type) {
|
|
67
|
+
case "memory": {
|
|
68
|
+
return new MemoryStorageAdapter(config.memory);
|
|
69
|
+
}
|
|
70
|
+
case "redis": {
|
|
71
|
+
const { RedisStorageAdapter } = await import("./adapters/redis.mjs");
|
|
72
|
+
return new RedisStorageAdapter(config.redis);
|
|
73
|
+
}
|
|
74
|
+
case "vercel-kv": {
|
|
75
|
+
const { VercelKvStorageAdapter } = await import("./adapters/vercel-kv.mjs");
|
|
76
|
+
return new VercelKvStorageAdapter(config.vercelKv);
|
|
77
|
+
}
|
|
78
|
+
case "upstash": {
|
|
79
|
+
const { UpstashStorageAdapter } = await import("./adapters/upstash.mjs");
|
|
80
|
+
return new UpstashStorageAdapter(config.upstash);
|
|
81
|
+
}
|
|
82
|
+
case "sqlite": {
|
|
83
|
+
const { SqliteStorageAdapter } = loadSqliteStorage();
|
|
84
|
+
return new SqliteStorageAdapter(parseSqliteOptions(config.sqlite));
|
|
85
|
+
}
|
|
86
|
+
case "cloudflare-kv": {
|
|
87
|
+
if (!config.cloudflareKv) {
|
|
88
|
+
throw new StorageConfigError(
|
|
89
|
+
"cloudflare-kv",
|
|
90
|
+
"cloudflare-kv requires `cloudflareKv: { namespace }` (the KV binding can't be auto-detected)"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const { CloudflareKvStorageAdapter } = await import("./adapters/cloudflare-kv.mjs");
|
|
94
|
+
return new CloudflareKvStorageAdapter(config.cloudflareKv);
|
|
95
|
+
}
|
|
96
|
+
case "auto":
|
|
97
|
+
throw new StorageConfigError("auto", "Auto type should be resolved before calling createAdapter");
|
|
98
|
+
default:
|
|
99
|
+
throw new StorageConfigError("unknown", `Unknown storage type: ${type}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function createStorage(config = {}) {
|
|
103
|
+
let type = config.type ?? "auto";
|
|
104
|
+
const fallback = config.fallback ?? (isProduction() ? "error" : "memory");
|
|
105
|
+
if (type === "auto") {
|
|
106
|
+
type = detectStorageType();
|
|
107
|
+
if (type === "memory" && isProduction()) warnNoDistributedStorage();
|
|
108
|
+
}
|
|
109
|
+
if (type === "sqlite") parseSqliteOptions(config.sqlite);
|
|
110
|
+
let adapter;
|
|
111
|
+
try {
|
|
112
|
+
adapter = await createAdapter(type, config);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
if (fallback === "memory" && type !== "memory") {
|
|
115
|
+
console.warn(
|
|
116
|
+
`[storage] Warning: Failed to create ${type} adapter, falling back to memory. Error: ${e instanceof Error ? e.message : String(e)}`
|
|
117
|
+
);
|
|
118
|
+
adapter = new MemoryStorageAdapter(config.memory);
|
|
119
|
+
} else {
|
|
120
|
+
throw e;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
await adapter.connect();
|
|
125
|
+
} catch (e) {
|
|
126
|
+
if (fallback === "memory" && type !== "memory") {
|
|
127
|
+
console.warn(
|
|
128
|
+
`[storage] Warning: Failed to connect to ${type}, falling back to memory. Error: ${e instanceof Error ? e.message : String(e)}`
|
|
129
|
+
);
|
|
130
|
+
adapter = new MemoryStorageAdapter(config.memory);
|
|
131
|
+
await adapter.connect();
|
|
132
|
+
} else {
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (config.prefix) {
|
|
137
|
+
return createNamespacedStorage(adapter, config.prefix);
|
|
138
|
+
}
|
|
139
|
+
return createRootStorage(adapter);
|
|
140
|
+
}
|
|
141
|
+
function createMemoryStorage(config = {}) {
|
|
142
|
+
const adapter = new MemoryStorageAdapter(config);
|
|
143
|
+
if (config.prefix) {
|
|
144
|
+
return createNamespacedStorage(adapter, config.prefix);
|
|
145
|
+
}
|
|
146
|
+
return createRootStorage(adapter);
|
|
147
|
+
}
|
|
148
|
+
function getDetectedStorageType() {
|
|
149
|
+
return detectStorageType();
|
|
150
|
+
}
|
|
151
|
+
export {
|
|
152
|
+
createMemoryStorage,
|
|
153
|
+
createStorage,
|
|
154
|
+
getDetectedStorageType
|
|
155
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// libs/utils/src/storage/index.ts
|
|
2
|
+
import { createStorage, createMemoryStorage, getDetectedStorageType } from "./factory.mjs";
|
|
3
|
+
import { createChangeFeed, DEFAULT_CHANGE_FEED_COALESCE_MS, DEFAULT_CHANGE_FEED_POLL_INTERVAL_MS } from "./change-feed.mjs";
|
|
4
|
+
import { attachRedisErrorListener, DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS } from "./redis-error-listener.mjs";
|
|
5
|
+
import { mergeRedisUrlFields, describeRedisUrlConflicts } from "./redis-url.mjs";
|
|
6
|
+
import {
|
|
7
|
+
NamespacedStorageImpl,
|
|
8
|
+
createRootStorage,
|
|
9
|
+
createNamespacedStorage,
|
|
10
|
+
buildPrefix,
|
|
11
|
+
NAMESPACE_SEPARATOR
|
|
12
|
+
} from "./namespace.mjs";
|
|
13
|
+
import {
|
|
14
|
+
StorageError,
|
|
15
|
+
StorageConnectionError,
|
|
16
|
+
StorageOperationError,
|
|
17
|
+
StorageNotSupportedError,
|
|
18
|
+
StorageConfigError,
|
|
19
|
+
StorageTTLError,
|
|
20
|
+
StoragePatternError,
|
|
21
|
+
StorageNotConnectedError,
|
|
22
|
+
EncryptedStorageError
|
|
23
|
+
} from "./errors.mjs";
|
|
24
|
+
import { TypedStorage } from "./typed-storage.mjs";
|
|
25
|
+
import { EncryptedTypedStorage } from "./encrypted-typed-storage.mjs";
|
|
26
|
+
import {
|
|
27
|
+
BaseStorageAdapter,
|
|
28
|
+
MemoryStorageAdapter,
|
|
29
|
+
RedisStorageAdapter,
|
|
30
|
+
createRedisClient,
|
|
31
|
+
VercelKvStorageAdapter,
|
|
32
|
+
createVercelKvClient,
|
|
33
|
+
UpstashStorageAdapter,
|
|
34
|
+
FileSystemStorageAdapter,
|
|
35
|
+
LocalStorageAdapter,
|
|
36
|
+
IndexedDBStorageAdapter
|
|
37
|
+
} from "./adapters/index.mjs";
|
|
38
|
+
import { globToRegex, matchesPattern, validatePattern, escapeGlob } from "./utils/pattern.mjs";
|
|
39
|
+
import {
|
|
40
|
+
MAX_TTL_SECONDS,
|
|
41
|
+
validateTTL,
|
|
42
|
+
validateOptionalTTL,
|
|
43
|
+
ttlToExpiresAt,
|
|
44
|
+
expiresAtToTTL,
|
|
45
|
+
isExpired,
|
|
46
|
+
normalizeTTL
|
|
47
|
+
} from "./utils/ttl.mjs";
|
|
48
|
+
export {
|
|
49
|
+
BaseStorageAdapter,
|
|
50
|
+
DEFAULT_CHANGE_FEED_COALESCE_MS,
|
|
51
|
+
DEFAULT_CHANGE_FEED_POLL_INTERVAL_MS,
|
|
52
|
+
DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS,
|
|
53
|
+
EncryptedStorageError,
|
|
54
|
+
EncryptedTypedStorage,
|
|
55
|
+
FileSystemStorageAdapter,
|
|
56
|
+
IndexedDBStorageAdapter,
|
|
57
|
+
LocalStorageAdapter,
|
|
58
|
+
MAX_TTL_SECONDS,
|
|
59
|
+
MemoryStorageAdapter,
|
|
60
|
+
NAMESPACE_SEPARATOR,
|
|
61
|
+
NamespacedStorageImpl,
|
|
62
|
+
RedisStorageAdapter,
|
|
63
|
+
StorageConfigError,
|
|
64
|
+
StorageConnectionError,
|
|
65
|
+
StorageError,
|
|
66
|
+
StorageNotConnectedError,
|
|
67
|
+
StorageNotSupportedError,
|
|
68
|
+
StorageOperationError,
|
|
69
|
+
StoragePatternError,
|
|
70
|
+
StorageTTLError,
|
|
71
|
+
TypedStorage,
|
|
72
|
+
UpstashStorageAdapter,
|
|
73
|
+
VercelKvStorageAdapter,
|
|
74
|
+
attachRedisErrorListener,
|
|
75
|
+
buildPrefix,
|
|
76
|
+
createChangeFeed,
|
|
77
|
+
createMemoryStorage,
|
|
78
|
+
createNamespacedStorage,
|
|
79
|
+
createRedisClient,
|
|
80
|
+
createRootStorage,
|
|
81
|
+
createStorage,
|
|
82
|
+
createVercelKvClient,
|
|
83
|
+
describeRedisUrlConflicts,
|
|
84
|
+
escapeGlob,
|
|
85
|
+
expiresAtToTTL,
|
|
86
|
+
getDetectedStorageType,
|
|
87
|
+
globToRegex,
|
|
88
|
+
isExpired,
|
|
89
|
+
matchesPattern,
|
|
90
|
+
mergeRedisUrlFields,
|
|
91
|
+
normalizeTTL,
|
|
92
|
+
ttlToExpiresAt,
|
|
93
|
+
validateOptionalTTL,
|
|
94
|
+
validatePattern,
|
|
95
|
+
validateTTL
|
|
96
|
+
};
|