@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,149 @@
|
|
|
1
|
+
// libs/utils/src/storage/adapters/cloudflare-kv.ts
|
|
2
|
+
import { StorageNotSupportedError, StorageOperationError } from "../errors.mjs";
|
|
3
|
+
import { matchesPattern } from "../utils/pattern.mjs";
|
|
4
|
+
import { validateTTL } from "../utils/ttl.mjs";
|
|
5
|
+
import { BaseStorageAdapter } from "./base.mjs";
|
|
6
|
+
var CF_KV_MIN_TTL_SECONDS = 60;
|
|
7
|
+
function literalPrefix(pattern) {
|
|
8
|
+
const star = pattern.indexOf("*");
|
|
9
|
+
const q = pattern.indexOf("?");
|
|
10
|
+
const idx = star === -1 ? q : q === -1 ? star : Math.min(star, q);
|
|
11
|
+
return idx === -1 ? pattern : pattern.slice(0, idx);
|
|
12
|
+
}
|
|
13
|
+
var CloudflareKvStorageAdapter = class extends BaseStorageAdapter {
|
|
14
|
+
backendName = "cloudflare-kv";
|
|
15
|
+
kv;
|
|
16
|
+
keyPrefix;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
super();
|
|
19
|
+
if (!options?.namespace) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"CloudflareKvStorageAdapter: a bound KV `namespace` is required (resolve it from the Worker `env`)."
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
this.kv = options.namespace;
|
|
25
|
+
this.keyPrefix = options.keyPrefix ?? "";
|
|
26
|
+
}
|
|
27
|
+
// \u2500\u2500 Lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
28
|
+
// A KV binding is always available; "connection" is just a readiness flag.
|
|
29
|
+
async connect() {
|
|
30
|
+
this.connected = true;
|
|
31
|
+
}
|
|
32
|
+
async disconnect() {
|
|
33
|
+
this.connected = false;
|
|
34
|
+
}
|
|
35
|
+
async ping() {
|
|
36
|
+
if (!this.connected) return false;
|
|
37
|
+
try {
|
|
38
|
+
await this.kv.list({ limit: 1 });
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// \u2500\u2500 Core \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
45
|
+
async get(key) {
|
|
46
|
+
this.ensureConnected();
|
|
47
|
+
return this.kv.get(this.prefixKey(key), "text");
|
|
48
|
+
}
|
|
49
|
+
async doSet(key, value, options) {
|
|
50
|
+
if (options?.ifNotExists || options?.ifExists) {
|
|
51
|
+
throw new StorageNotSupportedError(
|
|
52
|
+
"conditional set (ifNotExists/ifExists)",
|
|
53
|
+
this.backendName,
|
|
54
|
+
"Cloudflare KV has no compare-and-set; use a Durable Object for atomic conditional writes."
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const putOptions = options?.ttlSeconds !== void 0 ? { expirationTtl: this.toExpirationTtl(options.ttlSeconds, key) } : void 0;
|
|
58
|
+
await this.kv.put(this.prefixKey(key), value, putOptions);
|
|
59
|
+
}
|
|
60
|
+
async delete(key) {
|
|
61
|
+
this.ensureConnected();
|
|
62
|
+
const existed = await this.exists(key);
|
|
63
|
+
await this.kv.delete(this.prefixKey(key));
|
|
64
|
+
return existed;
|
|
65
|
+
}
|
|
66
|
+
async exists(key) {
|
|
67
|
+
this.ensureConnected();
|
|
68
|
+
return await this.kv.get(this.prefixKey(key), "text") !== null;
|
|
69
|
+
}
|
|
70
|
+
// \u2500\u2500 TTL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
71
|
+
async expire(key, ttlSeconds) {
|
|
72
|
+
this.ensureConnected();
|
|
73
|
+
const ttl = this.toExpirationTtl(ttlSeconds, key);
|
|
74
|
+
const value = await this.kv.get(this.prefixKey(key), "text");
|
|
75
|
+
if (value === null) return false;
|
|
76
|
+
await this.kv.put(this.prefixKey(key), value, { expirationTtl: ttl });
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
async ttl(_key) {
|
|
80
|
+
throw new StorageNotSupportedError(
|
|
81
|
+
"ttl",
|
|
82
|
+
this.backendName,
|
|
83
|
+
"Cloudflare KV does not expose a key's remaining TTL. Track expiry alongside the value if you need it."
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
// \u2500\u2500 Enumeration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
87
|
+
async keys(pattern = "*") {
|
|
88
|
+
this.ensureConnected();
|
|
89
|
+
const prefixedPattern = this.prefixKey(pattern);
|
|
90
|
+
const listPrefix = literalPrefix(prefixedPattern);
|
|
91
|
+
const out = [];
|
|
92
|
+
let cursor;
|
|
93
|
+
do {
|
|
94
|
+
const page = await this.kv.list({ prefix: listPrefix || void 0, cursor });
|
|
95
|
+
for (const { name } of page.keys) {
|
|
96
|
+
if (matchesPattern(name, prefixedPattern)) out.push(this.unprefixKey(name));
|
|
97
|
+
}
|
|
98
|
+
cursor = page.list_complete ? void 0 : page.cursor;
|
|
99
|
+
} while (cursor);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
// \u2500\u2500 Atomic (unsupported on KV) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
103
|
+
async incr(key) {
|
|
104
|
+
return this.incrBy(key, 1);
|
|
105
|
+
}
|
|
106
|
+
async decr(key) {
|
|
107
|
+
return this.incrBy(key, -1);
|
|
108
|
+
}
|
|
109
|
+
async incrBy(_key, _amount) {
|
|
110
|
+
throw new StorageNotSupportedError(
|
|
111
|
+
"incr/decr/incrBy",
|
|
112
|
+
this.backendName,
|
|
113
|
+
"Cloudflare KV has no atomic increment (a get+put is racy under eventual consistency). Use a Durable Object for atomic counters."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
async deleteIfEquals(_key, _expectedValue) {
|
|
117
|
+
throw new StorageNotSupportedError(
|
|
118
|
+
"deleteIfEquals",
|
|
119
|
+
this.backendName,
|
|
120
|
+
"Cloudflare KV has no compare-and-set; use a Durable Object for an atomic compare-and-delete."
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
// \u2500\u2500 Pub/Sub (unsupported) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
124
|
+
getPubSubSuggestion() {
|
|
125
|
+
return "Cloudflare KV does not support pub/sub. Use a Durable Object (or Queues) for fan-out.";
|
|
126
|
+
}
|
|
127
|
+
// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
128
|
+
/** Validate + enforce KV's 60s minimum `expirationTtl`. */
|
|
129
|
+
toExpirationTtl(ttlSeconds, key) {
|
|
130
|
+
validateTTL(ttlSeconds);
|
|
131
|
+
if (ttlSeconds < CF_KV_MIN_TTL_SECONDS) {
|
|
132
|
+
throw new StorageOperationError(
|
|
133
|
+
"set",
|
|
134
|
+
key,
|
|
135
|
+
`cloudflare-kv: expirationTtl must be >= ${CF_KV_MIN_TTL_SECONDS}s (Cloudflare KV minimum); got ${ttlSeconds}s.`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return ttlSeconds;
|
|
139
|
+
}
|
|
140
|
+
prefixKey(key) {
|
|
141
|
+
return this.keyPrefix + key;
|
|
142
|
+
}
|
|
143
|
+
unprefixKey(key) {
|
|
144
|
+
return this.keyPrefix && key.startsWith(this.keyPrefix) ? key.slice(this.keyPrefix.length) : key;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
export {
|
|
148
|
+
CloudflareKvStorageAdapter
|
|
149
|
+
};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// libs/utils/src/storage/adapters/filesystem.ts
|
|
2
|
+
import { BaseStorageAdapter } from "./base.mjs";
|
|
3
|
+
import { StorageOperationError } from "../errors.mjs";
|
|
4
|
+
import { globToRegex } from "../utils/pattern.mjs";
|
|
5
|
+
import { writeFile, readdir, unlink, fileExists, ensureDir, mkdir, rename, readJSON } from "../../fs/index.mjs";
|
|
6
|
+
import { randomBytes, bytesToHex } from "../../crypto/index.mjs";
|
|
7
|
+
var FileSystemStorageAdapter = class extends BaseStorageAdapter {
|
|
8
|
+
backendName = "filesystem";
|
|
9
|
+
baseDir;
|
|
10
|
+
extension;
|
|
11
|
+
dirMode;
|
|
12
|
+
fileMode;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
super();
|
|
15
|
+
this.baseDir = options.baseDir;
|
|
16
|
+
this.extension = options.extension ?? ".json";
|
|
17
|
+
this.dirMode = options.dirMode ?? 448;
|
|
18
|
+
this.fileMode = options.fileMode ?? 384;
|
|
19
|
+
}
|
|
20
|
+
// ============================================
|
|
21
|
+
// Connection Lifecycle
|
|
22
|
+
// ============================================
|
|
23
|
+
async connect() {
|
|
24
|
+
if (this.connected) return;
|
|
25
|
+
await ensureDir(this.baseDir);
|
|
26
|
+
try {
|
|
27
|
+
await mkdir(this.baseDir, { recursive: true, mode: this.dirMode });
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
this.connected = true;
|
|
31
|
+
}
|
|
32
|
+
async disconnect() {
|
|
33
|
+
if (!this.connected) return;
|
|
34
|
+
this.connected = false;
|
|
35
|
+
}
|
|
36
|
+
async ping() {
|
|
37
|
+
if (!this.connected) return false;
|
|
38
|
+
try {
|
|
39
|
+
return await fileExists(this.baseDir);
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ============================================
|
|
45
|
+
// Core Operations
|
|
46
|
+
// ============================================
|
|
47
|
+
async get(key) {
|
|
48
|
+
this.ensureConnected();
|
|
49
|
+
const filePath = this.keyToPath(key);
|
|
50
|
+
try {
|
|
51
|
+
const entry = await readJSON(filePath);
|
|
52
|
+
if (!entry) return null;
|
|
53
|
+
if (entry.expiresAt && Date.now() >= entry.expiresAt) {
|
|
54
|
+
await this.deleteFile(filePath);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return entry.value;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
const error = e;
|
|
60
|
+
if (error.code === "ENOENT") {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
throw e;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async doSet(key, value, options) {
|
|
67
|
+
const filePath = this.keyToPath(key);
|
|
68
|
+
const existingEntry = await this.readEntry(filePath);
|
|
69
|
+
const exists = existingEntry !== null && !this.isExpired(existingEntry);
|
|
70
|
+
if (options?.ifNotExists && exists) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (options?.ifExists && !exists) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const entry = { value };
|
|
77
|
+
if (options?.ttlSeconds) {
|
|
78
|
+
entry.expiresAt = Date.now() + options.ttlSeconds * 1e3;
|
|
79
|
+
}
|
|
80
|
+
await this.atomicWrite(filePath, entry);
|
|
81
|
+
}
|
|
82
|
+
async delete(key) {
|
|
83
|
+
this.ensureConnected();
|
|
84
|
+
const filePath = this.keyToPath(key);
|
|
85
|
+
return this.deleteFile(filePath);
|
|
86
|
+
}
|
|
87
|
+
async exists(key) {
|
|
88
|
+
this.ensureConnected();
|
|
89
|
+
const filePath = this.keyToPath(key);
|
|
90
|
+
try {
|
|
91
|
+
const entry = await this.readEntry(filePath);
|
|
92
|
+
if (!entry) return false;
|
|
93
|
+
if (this.isExpired(entry)) {
|
|
94
|
+
await this.deleteFile(filePath);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ============================================
|
|
103
|
+
// TTL Operations
|
|
104
|
+
// ============================================
|
|
105
|
+
async expire(key, ttlSeconds) {
|
|
106
|
+
this.ensureConnected();
|
|
107
|
+
const filePath = this.keyToPath(key);
|
|
108
|
+
const entry = await this.readEntry(filePath);
|
|
109
|
+
if (!entry || this.isExpired(entry)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
entry.expiresAt = Date.now() + ttlSeconds * 1e3;
|
|
113
|
+
await this.atomicWrite(filePath, entry);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
async ttl(key) {
|
|
117
|
+
this.ensureConnected();
|
|
118
|
+
const filePath = this.keyToPath(key);
|
|
119
|
+
const entry = await this.readEntry(filePath);
|
|
120
|
+
if (!entry) return null;
|
|
121
|
+
if (this.isExpired(entry)) {
|
|
122
|
+
await this.deleteFile(filePath);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
if (entry.expiresAt === void 0) {
|
|
126
|
+
return -1;
|
|
127
|
+
}
|
|
128
|
+
return Math.max(0, Math.ceil((entry.expiresAt - Date.now()) / 1e3));
|
|
129
|
+
}
|
|
130
|
+
// ============================================
|
|
131
|
+
// Key Enumeration
|
|
132
|
+
// ============================================
|
|
133
|
+
async keys(pattern = "*") {
|
|
134
|
+
this.ensureConnected();
|
|
135
|
+
const regex = globToRegex(pattern);
|
|
136
|
+
const result = [];
|
|
137
|
+
try {
|
|
138
|
+
const files = await readdir(this.baseDir);
|
|
139
|
+
for (const file of files) {
|
|
140
|
+
if (!file.endsWith(this.extension)) continue;
|
|
141
|
+
const key = this.pathToKey(file);
|
|
142
|
+
if (!regex.test(key)) continue;
|
|
143
|
+
const filePath = this.keyToPath(key);
|
|
144
|
+
const entry = await this.readEntry(filePath);
|
|
145
|
+
if (entry && !this.isExpired(entry)) {
|
|
146
|
+
result.push(key);
|
|
147
|
+
} else if (entry && this.isExpired(entry)) {
|
|
148
|
+
await this.deleteFile(filePath);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
const error = e;
|
|
153
|
+
if (error.code === "ENOENT") {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
throw e;
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
// ============================================
|
|
161
|
+
// Atomic Operations
|
|
162
|
+
// ============================================
|
|
163
|
+
async incr(key) {
|
|
164
|
+
return this.incrBy(key, 1);
|
|
165
|
+
}
|
|
166
|
+
async decr(key) {
|
|
167
|
+
return this.incrBy(key, -1);
|
|
168
|
+
}
|
|
169
|
+
async incrBy(key, amount) {
|
|
170
|
+
this.ensureConnected();
|
|
171
|
+
const filePath = this.keyToPath(key);
|
|
172
|
+
const entry = await this.readEntry(filePath);
|
|
173
|
+
let currentValue = 0;
|
|
174
|
+
if (entry && !this.isExpired(entry)) {
|
|
175
|
+
const parsed = parseInt(entry.value, 10);
|
|
176
|
+
if (isNaN(parsed)) {
|
|
177
|
+
throw new StorageOperationError("incrBy", key, "Value is not an integer");
|
|
178
|
+
}
|
|
179
|
+
currentValue = parsed;
|
|
180
|
+
}
|
|
181
|
+
const newValue = currentValue + amount;
|
|
182
|
+
const newEntry = { value: String(newValue) };
|
|
183
|
+
if (entry?.expiresAt && !this.isExpired(entry)) {
|
|
184
|
+
newEntry.expiresAt = entry.expiresAt;
|
|
185
|
+
}
|
|
186
|
+
await this.atomicWrite(filePath, newEntry);
|
|
187
|
+
return newValue;
|
|
188
|
+
}
|
|
189
|
+
// ============================================
|
|
190
|
+
// Internal Helpers
|
|
191
|
+
// ============================================
|
|
192
|
+
/**
|
|
193
|
+
* Convert a storage key to a file path.
|
|
194
|
+
* Sanitizes the key to be filesystem-safe.
|
|
195
|
+
*/
|
|
196
|
+
keyToPath(key) {
|
|
197
|
+
const safeKey = this.encodeKey(key);
|
|
198
|
+
return `${this.baseDir}/${safeKey}${this.extension}`;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Convert a filename back to a storage key.
|
|
202
|
+
*/
|
|
203
|
+
pathToKey(filename) {
|
|
204
|
+
const safeKey = filename.slice(0, -this.extension.length);
|
|
205
|
+
return this.decodeKey(safeKey);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Encode a key to be filesystem-safe.
|
|
209
|
+
* Uses URL encoding to handle special characters.
|
|
210
|
+
*/
|
|
211
|
+
encodeKey(key) {
|
|
212
|
+
return encodeURIComponent(key).replace(/\./g, "%2E");
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Decode a filesystem-safe key back to original.
|
|
216
|
+
*/
|
|
217
|
+
decodeKey(encoded) {
|
|
218
|
+
return decodeURIComponent(encoded);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Read an entry from a file.
|
|
222
|
+
*/
|
|
223
|
+
async readEntry(filePath) {
|
|
224
|
+
try {
|
|
225
|
+
return await readJSON(filePath);
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Check if an entry is expired.
|
|
232
|
+
*/
|
|
233
|
+
isExpired(entry) {
|
|
234
|
+
return entry.expiresAt !== void 0 && Date.now() >= entry.expiresAt;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Delete a file, returning true if it existed.
|
|
238
|
+
*/
|
|
239
|
+
async deleteFile(filePath) {
|
|
240
|
+
try {
|
|
241
|
+
await unlink(filePath);
|
|
242
|
+
return true;
|
|
243
|
+
} catch (e) {
|
|
244
|
+
const error = e;
|
|
245
|
+
if (error.code === "ENOENT") {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
throw e;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Atomic write: write to temp file then rename.
|
|
253
|
+
* This ensures we don't corrupt the file if write is interrupted.
|
|
254
|
+
*/
|
|
255
|
+
async atomicWrite(filePath, entry) {
|
|
256
|
+
const tempSuffix = bytesToHex(randomBytes(8));
|
|
257
|
+
const tempPath = `${filePath}.${tempSuffix}.tmp`;
|
|
258
|
+
try {
|
|
259
|
+
const content = JSON.stringify(entry, null, 2);
|
|
260
|
+
await writeFile(tempPath, content, { mode: this.fileMode });
|
|
261
|
+
await rename(tempPath, filePath);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
try {
|
|
264
|
+
await unlink(tempPath);
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
throw e;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Get suggestion message for pub/sub not supported error.
|
|
272
|
+
*/
|
|
273
|
+
getPubSubSuggestion() {
|
|
274
|
+
return "FileSystem adapter does not support pub/sub. Use Redis or Memory adapter for pub/sub support.";
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
export {
|
|
278
|
+
FileSystemStorageAdapter
|
|
279
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// libs/utils/src/storage/adapters/index.ts
|
|
2
|
+
import { BaseStorageAdapter } from "./base.mjs";
|
|
3
|
+
import { MemoryStorageAdapter } from "./memory.mjs";
|
|
4
|
+
import { RedisStorageAdapter, createRedisClient } from "./redis.mjs";
|
|
5
|
+
import { VercelKvStorageAdapter, createVercelKvClient } from "./vercel-kv.mjs";
|
|
6
|
+
import { UpstashStorageAdapter } from "./upstash.mjs";
|
|
7
|
+
import { CloudflareKvStorageAdapter } from "./cloudflare-kv.mjs";
|
|
8
|
+
import { FileSystemStorageAdapter } from "./filesystem.mjs";
|
|
9
|
+
import { LocalStorageAdapter } from "./localstorage.mjs";
|
|
10
|
+
import { IndexedDBStorageAdapter } from "./indexeddb.mjs";
|
|
11
|
+
export {
|
|
12
|
+
BaseStorageAdapter,
|
|
13
|
+
CloudflareKvStorageAdapter,
|
|
14
|
+
FileSystemStorageAdapter,
|
|
15
|
+
IndexedDBStorageAdapter,
|
|
16
|
+
LocalStorageAdapter,
|
|
17
|
+
MemoryStorageAdapter,
|
|
18
|
+
RedisStorageAdapter,
|
|
19
|
+
UpstashStorageAdapter,
|
|
20
|
+
VercelKvStorageAdapter,
|
|
21
|
+
createRedisClient,
|
|
22
|
+
createVercelKvClient
|
|
23
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// libs/utils/src/storage/adapters/indexeddb.ts
|
|
2
|
+
import { BaseStorageAdapter } from "./base.mjs";
|
|
3
|
+
import { matchesPattern } from "../utils/pattern.mjs";
|
|
4
|
+
var IndexedDBStorageAdapter = class extends BaseStorageAdapter {
|
|
5
|
+
backendName = "indexeddb";
|
|
6
|
+
dbName;
|
|
7
|
+
storeName;
|
|
8
|
+
prefix;
|
|
9
|
+
db = null;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super();
|
|
12
|
+
this.dbName = options?.dbName ?? "frontmcp";
|
|
13
|
+
this.storeName = options?.storeName ?? "kv";
|
|
14
|
+
this.prefix = options?.prefix ?? "frontmcp:";
|
|
15
|
+
}
|
|
16
|
+
assertAvailable() {
|
|
17
|
+
if (typeof indexedDB === "undefined") {
|
|
18
|
+
throw new Error("IndexedDB is not available in this environment");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
key(k) {
|
|
22
|
+
return `${this.prefix}${k}`;
|
|
23
|
+
}
|
|
24
|
+
stripPrefix(fullKey) {
|
|
25
|
+
return fullKey.slice(this.prefix.length);
|
|
26
|
+
}
|
|
27
|
+
// ============================================
|
|
28
|
+
// Connection Lifecycle
|
|
29
|
+
// ============================================
|
|
30
|
+
async connect() {
|
|
31
|
+
this.assertAvailable();
|
|
32
|
+
if (this.db) {
|
|
33
|
+
this.connected = true;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.db = await new Promise((resolve, reject) => {
|
|
37
|
+
const request = indexedDB.open(this.dbName, 1);
|
|
38
|
+
request.onupgradeneeded = () => {
|
|
39
|
+
const db = request.result;
|
|
40
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
41
|
+
db.createObjectStore(this.storeName, { keyPath: "k" });
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
request.onsuccess = () => resolve(request.result);
|
|
45
|
+
request.onerror = () => reject(request.error);
|
|
46
|
+
});
|
|
47
|
+
this.connected = true;
|
|
48
|
+
}
|
|
49
|
+
async disconnect() {
|
|
50
|
+
if (this.db) {
|
|
51
|
+
this.db.close();
|
|
52
|
+
this.db = null;
|
|
53
|
+
}
|
|
54
|
+
this.connected = false;
|
|
55
|
+
}
|
|
56
|
+
async ping() {
|
|
57
|
+
return this.db !== null && this.connected;
|
|
58
|
+
}
|
|
59
|
+
// ============================================
|
|
60
|
+
// Internal Helpers
|
|
61
|
+
// ============================================
|
|
62
|
+
getStore(mode) {
|
|
63
|
+
if (!this.db) {
|
|
64
|
+
throw new Error("IndexedDB adapter is not connected. Call connect() first.");
|
|
65
|
+
}
|
|
66
|
+
const tx = this.db.transaction(this.storeName, mode);
|
|
67
|
+
return tx.objectStore(this.storeName);
|
|
68
|
+
}
|
|
69
|
+
idbRequest(request) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
request.onsuccess = () => resolve(request.result);
|
|
72
|
+
request.onerror = () => reject(request.error);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
idbTransaction(store) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const tx = store.transaction;
|
|
78
|
+
tx.oncomplete = () => resolve();
|
|
79
|
+
tx.onerror = () => reject(tx.error);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// ============================================
|
|
83
|
+
// Core Operations
|
|
84
|
+
// ============================================
|
|
85
|
+
async get(key) {
|
|
86
|
+
this.ensureConnected();
|
|
87
|
+
const store = this.getStore("readonly");
|
|
88
|
+
const entry = await this.idbRequest(store.get(this.key(key)));
|
|
89
|
+
if (!entry) return null;
|
|
90
|
+
if (entry.e && entry.e < Date.now()) {
|
|
91
|
+
const writeStore = this.getStore("readwrite");
|
|
92
|
+
writeStore.delete(this.key(key));
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return entry.v;
|
|
96
|
+
}
|
|
97
|
+
async doSet(key, value, options) {
|
|
98
|
+
if (options?.ifNotExists) {
|
|
99
|
+
const existing = await this.get(key);
|
|
100
|
+
if (existing !== null) return;
|
|
101
|
+
}
|
|
102
|
+
if (options?.ifExists) {
|
|
103
|
+
const existing = await this.get(key);
|
|
104
|
+
if (existing === null) return;
|
|
105
|
+
}
|
|
106
|
+
const entry = { k: this.key(key), v: value };
|
|
107
|
+
if (options?.ttlSeconds) {
|
|
108
|
+
entry.e = Date.now() + options.ttlSeconds * 1e3;
|
|
109
|
+
}
|
|
110
|
+
const store = this.getStore("readwrite");
|
|
111
|
+
await this.idbRequest(store.put(entry));
|
|
112
|
+
}
|
|
113
|
+
async delete(key) {
|
|
114
|
+
this.ensureConnected();
|
|
115
|
+
const existing = await this.get(key);
|
|
116
|
+
if (existing === null) return false;
|
|
117
|
+
const store = this.getStore("readwrite");
|
|
118
|
+
await this.idbRequest(store.delete(this.key(key)));
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
async exists(key) {
|
|
122
|
+
return await this.get(key) !== null;
|
|
123
|
+
}
|
|
124
|
+
// ============================================
|
|
125
|
+
// TTL Operations
|
|
126
|
+
// ============================================
|
|
127
|
+
async expire(key, ttlSeconds) {
|
|
128
|
+
this.ensureConnected();
|
|
129
|
+
const value = await this.get(key);
|
|
130
|
+
if (value === null) return false;
|
|
131
|
+
await this.doSet(key, value, { ttlSeconds });
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
async ttl(key) {
|
|
135
|
+
this.ensureConnected();
|
|
136
|
+
const store = this.getStore("readonly");
|
|
137
|
+
const entry = await this.idbRequest(store.get(this.key(key)));
|
|
138
|
+
if (!entry) return null;
|
|
139
|
+
if (entry.e) {
|
|
140
|
+
const remaining = Math.ceil((entry.e - Date.now()) / 1e3);
|
|
141
|
+
return remaining > 0 ? remaining : null;
|
|
142
|
+
}
|
|
143
|
+
return -1;
|
|
144
|
+
}
|
|
145
|
+
// ============================================
|
|
146
|
+
// Key Enumeration
|
|
147
|
+
// ============================================
|
|
148
|
+
async keys(pattern) {
|
|
149
|
+
this.ensureConnected();
|
|
150
|
+
const store = this.getStore("readonly");
|
|
151
|
+
const allKeys = await this.idbRequest(store.getAllKeys());
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
const result = [];
|
|
154
|
+
for (const fullKey of allKeys) {
|
|
155
|
+
const keyStr = String(fullKey);
|
|
156
|
+
if (!keyStr.startsWith(this.prefix)) continue;
|
|
157
|
+
const key = this.stripPrefix(keyStr);
|
|
158
|
+
if (pattern && pattern !== "*" && !this.matchPattern(key, pattern)) continue;
|
|
159
|
+
const entry = await this.idbRequest(store.get(keyStr));
|
|
160
|
+
if (entry && (!entry.e || entry.e >= now)) {
|
|
161
|
+
result.push(key);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
// ============================================
|
|
167
|
+
// Atomic Operations
|
|
168
|
+
// ============================================
|
|
169
|
+
async incr(key) {
|
|
170
|
+
return this.incrBy(key, 1);
|
|
171
|
+
}
|
|
172
|
+
async decr(key) {
|
|
173
|
+
return this.incrBy(key, -1);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Increment by amount. NOT atomic \u2014 uses read-then-write which can race
|
|
177
|
+
* under concurrent access (e.g., multiple browser tabs).
|
|
178
|
+
*/
|
|
179
|
+
async incrBy(key, amount) {
|
|
180
|
+
this.ensureConnected();
|
|
181
|
+
const current = await this.get(key);
|
|
182
|
+
const value = current !== null ? parseInt(current, 10) + amount : amount;
|
|
183
|
+
if (Number.isNaN(value)) {
|
|
184
|
+
throw new Error(`Value at "${key}" is not an integer`);
|
|
185
|
+
}
|
|
186
|
+
await this.doSet(key, String(value));
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
// ============================================
|
|
190
|
+
// Pattern Matching
|
|
191
|
+
// ============================================
|
|
192
|
+
matchPattern(key, pattern) {
|
|
193
|
+
return matchesPattern(key, pattern);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
export {
|
|
197
|
+
IndexedDBStorageAdapter
|
|
198
|
+
};
|