@frontmcp/utils 1.10.16 → 1.11.0-rc.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/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,186 @@
|
|
|
1
|
+
// libs/utils/src/storage/namespace.ts
|
|
2
|
+
import { StorageNotSupportedError } from "./errors.mjs";
|
|
3
|
+
var NAMESPACE_SEPARATOR = ":";
|
|
4
|
+
function buildPrefix(name, id) {
|
|
5
|
+
if (id) {
|
|
6
|
+
return `${name}${NAMESPACE_SEPARATOR}${id}${NAMESPACE_SEPARATOR}`;
|
|
7
|
+
}
|
|
8
|
+
return `${name}${NAMESPACE_SEPARATOR}`;
|
|
9
|
+
}
|
|
10
|
+
var NamespacedStorageImpl = class _NamespacedStorageImpl {
|
|
11
|
+
constructor(adapter, prefix = "", root = adapter) {
|
|
12
|
+
this.adapter = adapter;
|
|
13
|
+
this.prefix = prefix;
|
|
14
|
+
this.root = root;
|
|
15
|
+
}
|
|
16
|
+
adapter;
|
|
17
|
+
prefix;
|
|
18
|
+
root;
|
|
19
|
+
// ============================================
|
|
20
|
+
// Key Prefixing Helpers
|
|
21
|
+
// ============================================
|
|
22
|
+
/**
|
|
23
|
+
* Add prefix to a key.
|
|
24
|
+
*/
|
|
25
|
+
prefixKey(key) {
|
|
26
|
+
return this.prefix + key;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Remove prefix from a key.
|
|
30
|
+
*/
|
|
31
|
+
unprefixKey(key) {
|
|
32
|
+
if (this.prefix && key.startsWith(this.prefix)) {
|
|
33
|
+
return key.slice(this.prefix.length);
|
|
34
|
+
}
|
|
35
|
+
return key;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Add prefix to a pattern (for keys() operation).
|
|
39
|
+
*/
|
|
40
|
+
prefixPattern(pattern) {
|
|
41
|
+
return this.prefix + pattern;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Add prefix to a channel (for pub/sub).
|
|
45
|
+
*/
|
|
46
|
+
prefixChannel(channel) {
|
|
47
|
+
return this.prefix + channel;
|
|
48
|
+
}
|
|
49
|
+
// ============================================
|
|
50
|
+
// Namespace API
|
|
51
|
+
// ============================================
|
|
52
|
+
namespace(name, id) {
|
|
53
|
+
const newPrefix = this.prefix + buildPrefix(name, id);
|
|
54
|
+
return new _NamespacedStorageImpl(this.adapter, newPrefix, this.root);
|
|
55
|
+
}
|
|
56
|
+
// ============================================
|
|
57
|
+
// Connection Lifecycle (delegated to root)
|
|
58
|
+
// ============================================
|
|
59
|
+
async connect() {
|
|
60
|
+
return this.adapter.connect();
|
|
61
|
+
}
|
|
62
|
+
async disconnect() {
|
|
63
|
+
return this.adapter.disconnect();
|
|
64
|
+
}
|
|
65
|
+
async ping() {
|
|
66
|
+
return this.adapter.ping();
|
|
67
|
+
}
|
|
68
|
+
// ============================================
|
|
69
|
+
// Core Operations (with prefixing)
|
|
70
|
+
// ============================================
|
|
71
|
+
async get(key) {
|
|
72
|
+
return this.adapter.get(this.prefixKey(key));
|
|
73
|
+
}
|
|
74
|
+
async set(key, value, options) {
|
|
75
|
+
return this.adapter.set(this.prefixKey(key), value, options);
|
|
76
|
+
}
|
|
77
|
+
async delete(key) {
|
|
78
|
+
return this.adapter.delete(this.prefixKey(key));
|
|
79
|
+
}
|
|
80
|
+
async exists(key) {
|
|
81
|
+
return this.adapter.exists(this.prefixKey(key));
|
|
82
|
+
}
|
|
83
|
+
// ============================================
|
|
84
|
+
// Batch Operations (with prefixing)
|
|
85
|
+
// ============================================
|
|
86
|
+
async mget(keys) {
|
|
87
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
88
|
+
return this.adapter.mget(prefixedKeys);
|
|
89
|
+
}
|
|
90
|
+
async mset(entries) {
|
|
91
|
+
const prefixedEntries = entries.map((e) => ({
|
|
92
|
+
...e,
|
|
93
|
+
key: this.prefixKey(e.key)
|
|
94
|
+
}));
|
|
95
|
+
return this.adapter.mset(prefixedEntries);
|
|
96
|
+
}
|
|
97
|
+
async mdelete(keys) {
|
|
98
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
99
|
+
return this.adapter.mdelete(prefixedKeys);
|
|
100
|
+
}
|
|
101
|
+
// ============================================
|
|
102
|
+
// TTL Operations (with prefixing)
|
|
103
|
+
// ============================================
|
|
104
|
+
async expire(key, ttlSeconds) {
|
|
105
|
+
return this.adapter.expire(this.prefixKey(key), ttlSeconds);
|
|
106
|
+
}
|
|
107
|
+
async ttl(key) {
|
|
108
|
+
return this.adapter.ttl(this.prefixKey(key));
|
|
109
|
+
}
|
|
110
|
+
// ============================================
|
|
111
|
+
// Key Enumeration (with prefixing)
|
|
112
|
+
// ============================================
|
|
113
|
+
async keys(pattern = "*") {
|
|
114
|
+
const prefixedPattern = this.prefixPattern(pattern);
|
|
115
|
+
const keys = await this.adapter.keys(prefixedPattern);
|
|
116
|
+
return keys.map((k) => this.unprefixKey(k));
|
|
117
|
+
}
|
|
118
|
+
async count(pattern = "*") {
|
|
119
|
+
const prefixedPattern = this.prefixPattern(pattern);
|
|
120
|
+
return this.adapter.count(prefixedPattern);
|
|
121
|
+
}
|
|
122
|
+
// ============================================
|
|
123
|
+
// Atomic Operations (with prefixing)
|
|
124
|
+
// ============================================
|
|
125
|
+
async incr(key) {
|
|
126
|
+
return this.adapter.incr(this.prefixKey(key));
|
|
127
|
+
}
|
|
128
|
+
async decr(key) {
|
|
129
|
+
return this.adapter.decr(this.prefixKey(key));
|
|
130
|
+
}
|
|
131
|
+
async incrBy(key, amount) {
|
|
132
|
+
return this.adapter.incrBy(this.prefixKey(key), amount);
|
|
133
|
+
}
|
|
134
|
+
async deleteIfEquals(key, expectedValue) {
|
|
135
|
+
if (!this.adapter.deleteIfEquals) {
|
|
136
|
+
throw new StorageNotSupportedError("deleteIfEquals", "this storage adapter");
|
|
137
|
+
}
|
|
138
|
+
return this.adapter.deleteIfEquals(this.prefixKey(key), expectedValue);
|
|
139
|
+
}
|
|
140
|
+
async expireIfEquals(key, expectedValue, ttlSeconds) {
|
|
141
|
+
if (!this.adapter.expireIfEquals) {
|
|
142
|
+
throw new StorageNotSupportedError("expireIfEquals", "this storage adapter");
|
|
143
|
+
}
|
|
144
|
+
return this.adapter.expireIfEquals(this.prefixKey(key), expectedValue, ttlSeconds);
|
|
145
|
+
}
|
|
146
|
+
async setIfEquals(key, expectedValue, value) {
|
|
147
|
+
if (!this.adapter.setIfEquals) {
|
|
148
|
+
throw new StorageNotSupportedError("setIfEquals", "this storage adapter");
|
|
149
|
+
}
|
|
150
|
+
return this.adapter.setIfEquals(this.prefixKey(key), expectedValue, value);
|
|
151
|
+
}
|
|
152
|
+
// ============================================
|
|
153
|
+
// Pub/Sub (with channel prefixing)
|
|
154
|
+
// ============================================
|
|
155
|
+
async publish(channel, message) {
|
|
156
|
+
return this.adapter.publish(this.prefixChannel(channel), message);
|
|
157
|
+
}
|
|
158
|
+
async subscribe(channel, handler) {
|
|
159
|
+
const prefixedChannel = this.prefixChannel(channel);
|
|
160
|
+
const wrappedHandler = (message, ch) => {
|
|
161
|
+
const unprefixedChannel = ch.startsWith(this.prefix) ? ch.slice(this.prefix.length) : ch;
|
|
162
|
+
handler(message, unprefixedChannel);
|
|
163
|
+
};
|
|
164
|
+
return this.adapter.subscribe(prefixedChannel, wrappedHandler);
|
|
165
|
+
}
|
|
166
|
+
onPubSubReconnect(listener) {
|
|
167
|
+
return this.adapter.onPubSubReconnect?.(listener) ?? (() => void 0);
|
|
168
|
+
}
|
|
169
|
+
supportsPubSub() {
|
|
170
|
+
return this.adapter.supportsPubSub();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
function createRootStorage(adapter) {
|
|
174
|
+
return new NamespacedStorageImpl(adapter, "", adapter);
|
|
175
|
+
}
|
|
176
|
+
function createNamespacedStorage(adapter, prefix) {
|
|
177
|
+
const normalizedPrefix = prefix && !prefix.endsWith(NAMESPACE_SEPARATOR) ? prefix + NAMESPACE_SEPARATOR : prefix;
|
|
178
|
+
return new NamespacedStorageImpl(adapter, normalizedPrefix, adapter);
|
|
179
|
+
}
|
|
180
|
+
export {
|
|
181
|
+
NAMESPACE_SEPARATOR,
|
|
182
|
+
NamespacedStorageImpl,
|
|
183
|
+
buildPrefix,
|
|
184
|
+
createNamespacedStorage,
|
|
185
|
+
createRootStorage
|
|
186
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// libs/utils/src/storage/redis-error-listener.ts
|
|
2
|
+
var DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS = 3e4;
|
|
3
|
+
function attachRedisErrorListener(client, options = {}) {
|
|
4
|
+
const label = options.label ?? "redis";
|
|
5
|
+
const intervalMs = options.intervalMs ?? DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS;
|
|
6
|
+
let lastLoggedAt = 0;
|
|
7
|
+
let suppressed = 0;
|
|
8
|
+
const listener = (error) => {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
if (lastLoggedAt !== 0 && now - lastLoggedAt < intervalMs) {
|
|
11
|
+
suppressed++;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const extra = suppressed > 0 ? ` (${suppressed} similar error(s) suppressed)` : "";
|
|
15
|
+
suppressed = 0;
|
|
16
|
+
lastLoggedAt = now;
|
|
17
|
+
const message = `[${label}] Redis connection error: ${error?.message ?? String(error)}${extra}`;
|
|
18
|
+
if (options.logger) {
|
|
19
|
+
options.logger.warn(message);
|
|
20
|
+
} else {
|
|
21
|
+
console.warn(message);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
client.on("error", listener);
|
|
25
|
+
return () => {
|
|
26
|
+
client.removeListener("error", listener);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS,
|
|
31
|
+
attachRedisErrorListener
|
|
32
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// libs/utils/src/storage/redis-url.ts
|
|
2
|
+
function decodeOrRaw(encoded) {
|
|
3
|
+
try {
|
|
4
|
+
return decodeURIComponent(encoded);
|
|
5
|
+
} catch {
|
|
6
|
+
return encoded;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function integerOrUndefined(text) {
|
|
10
|
+
const value = Number.parseInt(text ?? "", 10);
|
|
11
|
+
return Number.isNaN(value) ? void 0 : value;
|
|
12
|
+
}
|
|
13
|
+
function readRedisUrl(rawUrl) {
|
|
14
|
+
let url;
|
|
15
|
+
try {
|
|
16
|
+
url = new URL(rawUrl);
|
|
17
|
+
} catch {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
if (url.protocol !== "redis:" && url.protocol !== "rediss:") return void 0;
|
|
21
|
+
const query = Object.fromEntries(url.searchParams);
|
|
22
|
+
const { host, port, username, password, db, ...otherOptions } = query;
|
|
23
|
+
const queryOptions = {};
|
|
24
|
+
for (const [name, value = ""] of Object.entries(otherOptions)) {
|
|
25
|
+
queryOptions[name] = name === "family" ? integerOrUndefined(value) ?? value : value;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
host: url.hostname.replace(/^\[(.*)\]$/, "$1") || host,
|
|
29
|
+
port: integerOrUndefined(url.port || port),
|
|
30
|
+
username: decodeOrRaw(url.username) || username || void 0,
|
|
31
|
+
password: decodeOrRaw(url.password) || password || void 0,
|
|
32
|
+
db: integerOrUndefined(url.pathname.replace(/^\/+/, "") || db),
|
|
33
|
+
tls: url.protocol === "rediss:",
|
|
34
|
+
queryOptions
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function mergeRedisUrlFields(rawUrl, fields) {
|
|
38
|
+
const connection = readRedisUrl(rawUrl);
|
|
39
|
+
if (!connection) return void 0;
|
|
40
|
+
const fillIns = {};
|
|
41
|
+
const conflicts = [];
|
|
42
|
+
if (fields.host !== void 0 && fields.host !== connection.host) conflicts.push("host");
|
|
43
|
+
if (fields.port !== void 0) {
|
|
44
|
+
if (connection.port === void 0) fillIns.port = fields.port;
|
|
45
|
+
else if (fields.port !== connection.port) conflicts.push("port");
|
|
46
|
+
}
|
|
47
|
+
if (fields.password !== void 0) {
|
|
48
|
+
if (connection.password === void 0) fillIns.password = fields.password;
|
|
49
|
+
else if (fields.password !== connection.password) conflicts.push("password");
|
|
50
|
+
}
|
|
51
|
+
if (fields.db !== void 0) {
|
|
52
|
+
if (connection.db === void 0) fillIns.db = fields.db;
|
|
53
|
+
else if (fields.db !== connection.db) conflicts.push("db");
|
|
54
|
+
}
|
|
55
|
+
if (fields.tls === true && !connection.tls) fillIns.tls = true;
|
|
56
|
+
if (fields.tls === false && connection.tls) conflicts.push("tls");
|
|
57
|
+
return { connection, fillIns, conflicts };
|
|
58
|
+
}
|
|
59
|
+
function describeRedisUrlConflicts(conflicts) {
|
|
60
|
+
return `redis ${conflicts.join(", ")} contradict${conflicts.length === 1 ? "s" : ""} redis.url. Fields beside a url only fill in what the URL leaves out (port, password, db, tls); put the value in the URL or drop the field.`;
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
describeRedisUrlConflicts,
|
|
64
|
+
mergeRedisUrlFields,
|
|
65
|
+
readRedisUrl
|
|
66
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// libs/utils/src/storage/typed-storage.ts
|
|
2
|
+
var TypedStorage = class {
|
|
3
|
+
storage;
|
|
4
|
+
serialize;
|
|
5
|
+
deserialize;
|
|
6
|
+
schema;
|
|
7
|
+
throwOnInvalid;
|
|
8
|
+
constructor(storage, options = {}) {
|
|
9
|
+
this.storage = storage;
|
|
10
|
+
this.serialize = options.serialize ?? JSON.stringify;
|
|
11
|
+
this.deserialize = options.deserialize ?? JSON.parse;
|
|
12
|
+
this.schema = options.schema;
|
|
13
|
+
this.throwOnInvalid = options.throwOnInvalid ?? false;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get a typed value by key.
|
|
17
|
+
*
|
|
18
|
+
* @param key - Storage key
|
|
19
|
+
* @returns The typed value, or null if not found or invalid
|
|
20
|
+
*/
|
|
21
|
+
async get(key) {
|
|
22
|
+
const raw = await this.storage.get(key);
|
|
23
|
+
if (raw === null) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return this.parseValue(raw, key);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Set a typed value with optional TTL.
|
|
30
|
+
*
|
|
31
|
+
* @param key - Storage key
|
|
32
|
+
* @param value - Typed value to store
|
|
33
|
+
* @param options - Optional TTL and conditional flags
|
|
34
|
+
*/
|
|
35
|
+
async set(key, value, options) {
|
|
36
|
+
const serialized = this.serialize(value);
|
|
37
|
+
await this.storage.set(key, serialized, options);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Delete a key.
|
|
41
|
+
*
|
|
42
|
+
* @param key - Storage key
|
|
43
|
+
* @returns true if key existed and was deleted
|
|
44
|
+
*/
|
|
45
|
+
async delete(key) {
|
|
46
|
+
return this.storage.delete(key);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check if a key exists.
|
|
50
|
+
*
|
|
51
|
+
* @param key - Storage key
|
|
52
|
+
* @returns true if key exists
|
|
53
|
+
*/
|
|
54
|
+
async exists(key) {
|
|
55
|
+
return this.storage.exists(key);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get multiple typed values.
|
|
59
|
+
*
|
|
60
|
+
* @param keys - Array of storage keys
|
|
61
|
+
* @returns Array of typed values (null for missing/invalid keys)
|
|
62
|
+
*/
|
|
63
|
+
async mget(keys) {
|
|
64
|
+
if (keys.length === 0) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const rawValues = await this.storage.mget(keys);
|
|
68
|
+
return rawValues.map((raw, index) => {
|
|
69
|
+
if (raw === null) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return this.parseValue(raw, keys[index]);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Set multiple typed values.
|
|
77
|
+
*
|
|
78
|
+
* @param entries - Array of key-value-options entries
|
|
79
|
+
*/
|
|
80
|
+
async mset(entries) {
|
|
81
|
+
if (entries.length === 0) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const rawEntries = entries.map((entry) => ({
|
|
85
|
+
key: entry.key,
|
|
86
|
+
value: this.serialize(entry.value),
|
|
87
|
+
options: entry.options
|
|
88
|
+
}));
|
|
89
|
+
await this.storage.mset(rawEntries);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Delete multiple keys.
|
|
93
|
+
*
|
|
94
|
+
* @param keys - Array of storage keys
|
|
95
|
+
* @returns Number of keys actually deleted
|
|
96
|
+
*/
|
|
97
|
+
async mdelete(keys) {
|
|
98
|
+
return this.storage.mdelete(keys);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Update TTL on an existing key.
|
|
102
|
+
*
|
|
103
|
+
* @param key - Storage key
|
|
104
|
+
* @param ttlSeconds - New TTL in seconds
|
|
105
|
+
* @returns true if key exists and TTL was set
|
|
106
|
+
*/
|
|
107
|
+
async expire(key, ttlSeconds) {
|
|
108
|
+
return this.storage.expire(key, ttlSeconds);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get remaining TTL for a key.
|
|
112
|
+
*
|
|
113
|
+
* @param key - Storage key
|
|
114
|
+
* @returns TTL in seconds, -1 if no TTL, or null if key doesn't exist
|
|
115
|
+
*/
|
|
116
|
+
async ttl(key) {
|
|
117
|
+
return this.storage.ttl(key);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* List keys matching a pattern.
|
|
121
|
+
*
|
|
122
|
+
* @param pattern - Glob pattern (default: '*' for all keys)
|
|
123
|
+
* @returns Array of matching keys
|
|
124
|
+
*/
|
|
125
|
+
async keys(pattern) {
|
|
126
|
+
return this.storage.keys(pattern);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Count keys matching a pattern.
|
|
130
|
+
*
|
|
131
|
+
* @param pattern - Glob pattern (default: '*' for all keys)
|
|
132
|
+
* @returns Number of matching keys
|
|
133
|
+
*/
|
|
134
|
+
async count(pattern) {
|
|
135
|
+
return this.storage.count(pattern);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get the underlying storage adapter.
|
|
139
|
+
* Use with caution - operations bypass type safety.
|
|
140
|
+
*/
|
|
141
|
+
get raw() {
|
|
142
|
+
return this.storage;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Parse and validate a raw value.
|
|
146
|
+
*/
|
|
147
|
+
parseValue(raw, key) {
|
|
148
|
+
try {
|
|
149
|
+
const parsed = this.deserialize(raw);
|
|
150
|
+
if (this.schema) {
|
|
151
|
+
const result = this.schema.safeParse(parsed);
|
|
152
|
+
if (!result.success) {
|
|
153
|
+
if (this.throwOnInvalid) {
|
|
154
|
+
throw new Error(`TypedStorage validation failed for key "${key}": ${result.error.message}`);
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return result.data;
|
|
159
|
+
}
|
|
160
|
+
return parsed;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (this.throwOnInvalid) {
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
export {
|
|
170
|
+
TypedStorage
|
|
171
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// libs/utils/src/storage/utils/compare-and-delete.ts
|
|
2
|
+
var COMPARE_AND_DELETE_SCRIPT = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0`;
|
|
3
|
+
var COMPARE_AND_EXPIRE_SCRIPT = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('EXPIRE', KEYS[1], ARGV[2]) end return 0`;
|
|
4
|
+
var COMPARE_AND_SET_SCRIPT = `if redis.call('GET', KEYS[1]) == ARGV[1] then redis.call('SET', KEYS[1], ARGV[2]) return 1 end return 0`;
|
|
5
|
+
export {
|
|
6
|
+
COMPARE_AND_DELETE_SCRIPT,
|
|
7
|
+
COMPARE_AND_EXPIRE_SCRIPT,
|
|
8
|
+
COMPARE_AND_SET_SCRIPT
|
|
9
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// libs/utils/src/storage/utils/index.ts
|
|
2
|
+
import { globToRegex, matchesPattern, validatePattern, escapeGlob } from "./pattern.mjs";
|
|
3
|
+
import {
|
|
4
|
+
MAX_TTL_SECONDS,
|
|
5
|
+
validateTTL,
|
|
6
|
+
validateOptionalTTL,
|
|
7
|
+
ttlToExpiresAt,
|
|
8
|
+
expiresAtToTTL,
|
|
9
|
+
isExpired,
|
|
10
|
+
normalizeTTL
|
|
11
|
+
} from "./ttl.mjs";
|
|
12
|
+
export {
|
|
13
|
+
MAX_TTL_SECONDS,
|
|
14
|
+
escapeGlob,
|
|
15
|
+
expiresAtToTTL,
|
|
16
|
+
globToRegex,
|
|
17
|
+
isExpired,
|
|
18
|
+
matchesPattern,
|
|
19
|
+
normalizeTTL,
|
|
20
|
+
ttlToExpiresAt,
|
|
21
|
+
validateOptionalTTL,
|
|
22
|
+
validatePattern,
|
|
23
|
+
validateTTL
|
|
24
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// libs/utils/src/storage/utils/pattern.ts
|
|
2
|
+
import { StoragePatternError } from "../errors.mjs";
|
|
3
|
+
var MAX_PATTERN_LENGTH = 500;
|
|
4
|
+
var MAX_WILDCARDS = 20;
|
|
5
|
+
var REGEX_SPECIAL_CHARS = /[.+^${}()|[\]\\]/g;
|
|
6
|
+
function globToRegex(pattern) {
|
|
7
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
8
|
+
throw new StoragePatternError(
|
|
9
|
+
pattern.substring(0, 50) + "...",
|
|
10
|
+
`Pattern exceeds maximum length of ${MAX_PATTERN_LENGTH} characters`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
const wildcardCount = (pattern.match(/[*?]/g) || []).length;
|
|
14
|
+
if (wildcardCount > MAX_WILDCARDS) {
|
|
15
|
+
throw new StoragePatternError(pattern, `Pattern has too many wildcards (max: ${MAX_WILDCARDS})`);
|
|
16
|
+
}
|
|
17
|
+
if (pattern === "" || pattern === "*") {
|
|
18
|
+
return /^.*$/;
|
|
19
|
+
}
|
|
20
|
+
let regexStr = "^";
|
|
21
|
+
let prevChar = "";
|
|
22
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
23
|
+
const char = pattern[i];
|
|
24
|
+
switch (char) {
|
|
25
|
+
case "*":
|
|
26
|
+
if (prevChar !== "*") {
|
|
27
|
+
regexStr += ".*";
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
case "?":
|
|
31
|
+
regexStr += ".";
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
regexStr += char.replace(REGEX_SPECIAL_CHARS, "\\$&");
|
|
35
|
+
}
|
|
36
|
+
prevChar = char;
|
|
37
|
+
}
|
|
38
|
+
regexStr += "$";
|
|
39
|
+
try {
|
|
40
|
+
return new RegExp(regexStr);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new StoragePatternError(pattern, "Failed to compile pattern to regex");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function matchesPattern(key, pattern) {
|
|
46
|
+
const regex = globToRegex(pattern);
|
|
47
|
+
return regex.test(key);
|
|
48
|
+
}
|
|
49
|
+
function validatePattern(pattern) {
|
|
50
|
+
try {
|
|
51
|
+
globToRegex(pattern);
|
|
52
|
+
return { valid: true };
|
|
53
|
+
} catch (e) {
|
|
54
|
+
return {
|
|
55
|
+
valid: false,
|
|
56
|
+
error: e instanceof Error ? e.message : "Invalid pattern"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function escapeGlob(literal) {
|
|
61
|
+
return literal.replace(/[*?\\]/g, "\\$&");
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
escapeGlob,
|
|
65
|
+
globToRegex,
|
|
66
|
+
matchesPattern,
|
|
67
|
+
validatePattern
|
|
68
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// libs/utils/src/storage/utils/ttl.ts
|
|
2
|
+
import { StorageTTLError } from "../errors.mjs";
|
|
3
|
+
var MAX_TTL_SECONDS = 31536e5;
|
|
4
|
+
function validateTTL(ttlSeconds) {
|
|
5
|
+
if (typeof ttlSeconds !== "number") {
|
|
6
|
+
throw new StorageTTLError(ttlSeconds, `TTL must be a number, got ${typeof ttlSeconds}`);
|
|
7
|
+
}
|
|
8
|
+
if (!Number.isFinite(ttlSeconds)) {
|
|
9
|
+
throw new StorageTTLError(ttlSeconds, "TTL must be a finite number");
|
|
10
|
+
}
|
|
11
|
+
if (!Number.isInteger(ttlSeconds)) {
|
|
12
|
+
throw new StorageTTLError(ttlSeconds, `TTL must be an integer, got ${ttlSeconds}`);
|
|
13
|
+
}
|
|
14
|
+
if (ttlSeconds <= 0) {
|
|
15
|
+
throw new StorageTTLError(ttlSeconds, `TTL must be positive, got ${ttlSeconds}`);
|
|
16
|
+
}
|
|
17
|
+
if (ttlSeconds > MAX_TTL_SECONDS) {
|
|
18
|
+
throw new StorageTTLError(ttlSeconds, `TTL exceeds maximum of ${MAX_TTL_SECONDS} seconds`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function validateOptionalTTL(ttlSeconds) {
|
|
22
|
+
if (ttlSeconds !== void 0) {
|
|
23
|
+
validateTTL(ttlSeconds);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function ttlToExpiresAt(ttlSeconds) {
|
|
27
|
+
return Date.now() + ttlSeconds * 1e3;
|
|
28
|
+
}
|
|
29
|
+
function expiresAtToTTL(expiresAt) {
|
|
30
|
+
const remaining = Math.ceil((expiresAt - Date.now()) / 1e3);
|
|
31
|
+
return Math.max(0, remaining);
|
|
32
|
+
}
|
|
33
|
+
function isExpired(expiresAt) {
|
|
34
|
+
if (expiresAt === void 0) return false;
|
|
35
|
+
return Date.now() >= expiresAt;
|
|
36
|
+
}
|
|
37
|
+
function normalizeTTL(ttl, unit) {
|
|
38
|
+
return unit === "milliseconds" ? Math.ceil(ttl / 1e3) : ttl;
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
MAX_TTL_SECONDS,
|
|
42
|
+
expiresAtToTTL,
|
|
43
|
+
isExpired,
|
|
44
|
+
normalizeTTL,
|
|
45
|
+
ttlToExpiresAt,
|
|
46
|
+
validateOptionalTTL,
|
|
47
|
+
validateTTL
|
|
48
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// libs/utils/src/uri/index.ts
|
|
2
|
+
import { isValidMcpUri, extractUriScheme, isValidMcpUriTemplate } from "./uri-validation.mjs";
|
|
3
|
+
import {
|
|
4
|
+
parseUriTemplate,
|
|
5
|
+
matchUriTemplate,
|
|
6
|
+
expandUriTemplate,
|
|
7
|
+
extractTemplateParams,
|
|
8
|
+
isUriTemplate
|
|
9
|
+
} from "./uri-template.mjs";
|
|
10
|
+
export {
|
|
11
|
+
expandUriTemplate,
|
|
12
|
+
extractTemplateParams,
|
|
13
|
+
extractUriScheme,
|
|
14
|
+
isUriTemplate,
|
|
15
|
+
isValidMcpUri,
|
|
16
|
+
isValidMcpUriTemplate,
|
|
17
|
+
matchUriTemplate,
|
|
18
|
+
parseUriTemplate
|
|
19
|
+
};
|