@frontmcp/plugin-remember 1.7.2 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/index.mjs CHANGED
@@ -20,887 +20,1084 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
20
20
  // plugins/plugin-remember/src/remember.plugin.ts
21
21
  import {
22
22
  DynamicPlugin,
23
- Plugin,
24
- ProviderScope as ProviderScope5,
23
+ FRONTMCP_CONTEXT,
25
24
  FrontMcpConfig,
26
- FRONTMCP_CONTEXT as FRONTMCP_CONTEXT2,
25
+ FrontMcpLogger,
27
26
  getGlobalStoreConfig,
28
- isVercelKvProvider
27
+ isVercelKvProvider,
28
+ Plugin,
29
+ ProviderScope as ProviderScope5
29
30
  } from "@frontmcp/sdk";
30
31
 
31
- // plugins/plugin-remember/src/remember.symbols.ts
32
- var RememberStoreToken = /* @__PURE__ */ Symbol(
33
- "plugin:remember:store"
34
- );
35
- var RememberConfigToken = /* @__PURE__ */ Symbol(
36
- "plugin:remember:config"
37
- );
38
- var RememberAccessorToken = /* @__PURE__ */ Symbol(
39
- "plugin:remember:accessor"
40
- );
32
+ // plugins/plugin-remember/src/providers/remember-accessor.provider.ts
33
+ import {
34
+ Provider,
35
+ ProviderScope,
36
+ STATELESS_SESSION_ID
37
+ } from "@frontmcp/sdk";
41
38
 
42
- // plugins/plugin-remember/src/providers/remember-memory.provider.ts
43
- import { Provider, ProviderScope } from "@frontmcp/sdk";
44
- var MAX_TIMEOUT_MS = 2 ** 31 - 1;
45
- var RememberMemoryProvider = class {
46
- memory = /* @__PURE__ */ new Map();
47
- sweeper;
48
- constructor(sweepIntervalSeconds = 60) {
49
- this.sweeper = setInterval(() => this.sweep(), sweepIntervalSeconds * 1e3);
50
- this.sweeper.unref?.();
39
+ // plugins/plugin-remember/src/remember.crypto.ts
40
+ import {
41
+ base64urlDecode,
42
+ base64urlEncode as base64urlEncode2,
43
+ decryptAesGcm,
44
+ encryptAesGcm,
45
+ hkdfSha256,
46
+ randomBytes as randomBytes2
47
+ } from "@frontmcp/utils";
48
+
49
+ // plugins/plugin-remember/src/remember.secret-persistence.ts
50
+ import * as path from "path";
51
+ import { z } from "@frontmcp/lazy-zod";
52
+ import { base64urlEncode, mkdir, randomBytes, readFile, rename, unlink, writeFile } from "@frontmcp/utils";
53
+ var DEFAULT_SECRET_FILE_PATH = ".frontmcp/remember-secret.json";
54
+ var SECRET_BYTES = 32;
55
+ var rememberSecretDataSchema = z.object({
56
+ secret: z.string().min(32),
57
+ // base64url of 32 bytes is ~43 chars
58
+ createdAt: z.number().positive().int(),
59
+ version: z.literal(1)
60
+ });
61
+ function validateSecretData(data) {
62
+ const result = rememberSecretDataSchema.safeParse(data);
63
+ if (!result.success) {
64
+ return { valid: false, error: result.error.issues[0]?.message ?? "Invalid secret structure" };
51
65
  }
52
- /**
53
- * Store a value with optional TTL.
54
- * Always JSON-serializes the value for consistent storage/retrieval.
55
- */
56
- async setValue(key, value, ttlSeconds) {
57
- const strValue = JSON.stringify(value);
58
- const existing = this.memory.get(key);
59
- if (existing?.timeout) clearTimeout(existing.timeout);
60
- const entry = { value: strValue };
61
- if (ttlSeconds && ttlSeconds > 0) {
62
- const ttlMs = ttlSeconds * 1e3;
63
- entry.expiresAt = Date.now() + ttlMs;
64
- if (ttlMs <= MAX_TIMEOUT_MS) {
65
- entry.timeout = setTimeout(() => {
66
- const e = this.memory.get(key);
67
- if (e && e.expiresAt && e.expiresAt <= Date.now()) {
68
- this.memory.delete(key);
69
- }
70
- }, ttlMs);
71
- entry.timeout.unref?.();
72
- }
73
- }
74
- this.memory.set(key, entry);
66
+ const parsed = result.data;
67
+ const now = Date.now();
68
+ if (parsed.createdAt > now + 6e4) {
69
+ return { valid: false, error: "createdAt is in the future" };
75
70
  }
76
- /**
77
- * Retrieve a value by key.
78
- * JSON-parses the stored value to match the setValue serialization.
79
- */
80
- async getValue(key, defaultValue) {
81
- const entry = this.memory.get(key);
82
- if (!entry) return defaultValue;
83
- if (this.isExpired(entry)) {
84
- await this.delete(key);
85
- return defaultValue;
86
- }
87
- try {
88
- return JSON.parse(entry.value);
89
- } catch {
90
- return entry.value;
91
- }
71
+ const hundredYearsMs = 100 * 365 * 24 * 60 * 60 * 1e3;
72
+ if (parsed.createdAt < now - hundredYearsMs) {
73
+ return { valid: false, error: "createdAt is too old" };
92
74
  }
93
- /**
94
- * Delete a key.
95
- */
96
- async delete(key) {
97
- const entry = this.memory.get(key);
98
- if (entry?.timeout) clearTimeout(entry.timeout);
99
- this.memory.delete(key);
75
+ return { valid: true };
76
+ }
77
+ function isSecretPersistenceEnabled(options) {
78
+ const isProduction = process.env["NODE_ENV"] === "production";
79
+ if (isProduction) {
80
+ return options?.forceEnable === true;
100
81
  }
101
- /**
102
- * Check if a key exists (and is not expired).
103
- */
104
- async exists(key) {
105
- const entry = this.memory.get(key);
106
- if (!entry) return false;
107
- if (this.isExpired(entry)) {
108
- await this.delete(key);
109
- return false;
110
- }
111
- return true;
82
+ return true;
83
+ }
84
+ function resolveSecretPath(options) {
85
+ const secretPath = options?.secretPath ?? DEFAULT_SECRET_FILE_PATH;
86
+ if (path.isAbsolute(secretPath)) {
87
+ return secretPath;
112
88
  }
113
- /**
114
- * List keys matching a glob-style pattern.
115
- * Supports * and ? wildcards.
116
- */
117
- async keys(pattern) {
118
- const result = [];
119
- const regex = pattern ? this.patternToRegex(pattern) : null;
120
- const expiredKeys = [];
121
- for (const [key, entry] of this.memory) {
122
- if (this.isExpired(entry)) {
123
- expiredKeys.push(key);
124
- }
125
- }
126
- for (const key of expiredKeys) {
127
- await this.delete(key);
89
+ return path.resolve(process.cwd(), secretPath);
90
+ }
91
+ async function loadRememberSecret(options) {
92
+ if (!isSecretPersistenceEnabled(options)) {
93
+ return null;
94
+ }
95
+ const secretPath = resolveSecretPath(options);
96
+ try {
97
+ const content = await readFile(secretPath, "utf8");
98
+ const data = JSON.parse(content);
99
+ const validation = validateSecretData(data);
100
+ if (!validation.valid) {
101
+ console.warn(
102
+ `[RememberSecretPersistence] Invalid secret file format at ${secretPath}: ${validation.error}, will regenerate`
103
+ );
104
+ return null;
128
105
  }
129
- for (const [key, entry] of this.memory) {
130
- if (this.isExpired(entry)) {
131
- continue;
132
- }
133
- if (regex && !regex.test(key)) {
134
- continue;
135
- }
136
- result.push(key);
106
+ return data;
107
+ } catch (error) {
108
+ if (error.code === "ENOENT") {
109
+ return null;
137
110
  }
138
- return result;
111
+ console.warn(`[RememberSecretPersistence] Failed to load secret from ${secretPath}: ${error.message}`);
112
+ return null;
139
113
  }
140
- /**
141
- * Gracefully close the provider.
142
- */
143
- async close() {
144
- if (this.sweeper) clearInterval(this.sweeper);
145
- for (const [, entry] of this.memory) {
146
- if (entry.timeout) clearTimeout(entry.timeout);
114
+ }
115
+ async function saveRememberSecret(secretData, options) {
116
+ if (!isSecretPersistenceEnabled(options)) {
117
+ return true;
118
+ }
119
+ const secretPath = resolveSecretPath(options);
120
+ const dir = path.dirname(secretPath);
121
+ const tempPath = `${secretPath}.tmp.${Date.now()}.${base64urlEncode(randomBytes(8))}`;
122
+ try {
123
+ await mkdir(dir, { recursive: true, mode: 448 });
124
+ const content = JSON.stringify(secretData, null, 2);
125
+ await writeFile(tempPath, content, { mode: 384 });
126
+ await rename(tempPath, secretPath);
127
+ return true;
128
+ } catch (error) {
129
+ console.error(`[RememberSecretPersistence] Failed to save secret to ${secretPath}: ${error.message}`);
130
+ try {
131
+ await unlink(tempPath);
132
+ } catch {
147
133
  }
148
- this.memory.clear();
134
+ return false;
149
135
  }
150
- // ─────────────────────────────────────────────────────────────────────────────
151
- // Private Methods
152
- // ─────────────────────────────────────────────────────────────────────────────
153
- isExpired(entry) {
154
- return entry.expiresAt !== void 0 && entry.expiresAt <= Date.now();
136
+ }
137
+ var cachedSecret = null;
138
+ var secretGenerationPromise = null;
139
+ function generateSecret() {
140
+ return base64urlEncode(randomBytes(SECRET_BYTES));
141
+ }
142
+ async function getOrCreatePersistedSecret(options) {
143
+ if (cachedSecret) {
144
+ return cachedSecret;
155
145
  }
156
- /**
157
- * Periodically remove expired keys to keep memory tidy.
158
- */
159
- sweep() {
160
- const now = Date.now();
161
- for (const [key, entry] of this.memory) {
162
- if (entry.expiresAt !== void 0 && entry.expiresAt <= now) {
163
- if (entry.timeout) clearTimeout(entry.timeout);
164
- this.memory.delete(key);
146
+ if (secretGenerationPromise) {
147
+ return secretGenerationPromise;
148
+ }
149
+ secretGenerationPromise = (async () => {
150
+ try {
151
+ const loaded = await loadRememberSecret(options);
152
+ if (loaded) {
153
+ cachedSecret = loaded.secret;
154
+ return cachedSecret;
155
+ }
156
+ const newSecret = generateSecret();
157
+ const secretData = {
158
+ secret: newSecret,
159
+ createdAt: Date.now(),
160
+ version: 1
161
+ };
162
+ if (isSecretPersistenceEnabled(options)) {
163
+ await saveRememberSecret(secretData, options);
164
+ } else {
165
+ console.warn(
166
+ "[RememberSecretPersistence] No REMEMBER_SECRET (or MCP_MEMORY_SECRET / MCP_SESSION_SECRET) is set in production, so a random in-memory secret is used. Encrypted memory will be unreadable after a restart and by other instances sharing the store. Set REMEMBER_SECRET to the same value on every instance."
167
+ );
165
168
  }
169
+ cachedSecret = newSecret;
170
+ return cachedSecret;
171
+ } finally {
172
+ secretGenerationPromise = null;
166
173
  }
174
+ })();
175
+ return secretGenerationPromise;
176
+ }
177
+
178
+ // plugins/plugin-remember/src/remember.crypto.ts
179
+ async function getBaseSecret() {
180
+ const envSecret = process.env["REMEMBER_SECRET"] || process.env["MCP_MEMORY_SECRET"] || process.env["MCP_SESSION_SECRET"];
181
+ if (envSecret) {
182
+ return envSecret;
167
183
  }
168
- /**
169
- * Convert a glob-style pattern to a RegExp.
170
- * * matches any sequence of characters
171
- * ? matches any single character
172
- *
173
- * Includes ReDoS protection: validates pattern length and wildcard count.
174
- */
175
- patternToRegex(pattern) {
176
- if (pattern.length > RememberMemoryProvider.MAX_PATTERN_LENGTH) {
177
- throw new Error(`Pattern too long: ${pattern.length} chars (max ${RememberMemoryProvider.MAX_PATTERN_LENGTH})`);
178
- }
179
- const wildcardCount = (pattern.match(/[*?]/g) || []).length;
180
- if (wildcardCount > RememberMemoryProvider.MAX_WILDCARDS) {
181
- throw new Error(`Too many wildcards in pattern: ${wildcardCount} (max ${RememberMemoryProvider.MAX_WILDCARDS})`);
184
+ return getOrCreatePersistedSecret();
185
+ }
186
+ var textEncoder = new TextEncoder();
187
+ async function deriveEncryptionKey(source) {
188
+ const salt = textEncoder.encode("remember-plugin-v1");
189
+ let ikm;
190
+ let context;
191
+ switch (source.type) {
192
+ case "session":
193
+ ikm = await getBaseSecret() + source.sessionId;
194
+ context = `remember:session:${source.sessionId}`;
195
+ break;
196
+ case "tool":
197
+ ikm = await getBaseSecret() + source.sessionId;
198
+ context = `remember:tool:${source.toolName}:${source.sessionId}`;
199
+ break;
200
+ case "user":
201
+ ikm = await getBaseSecret() + source.userId;
202
+ context = `remember:user:${source.userId}`;
203
+ break;
204
+ case "global":
205
+ ikm = await getBaseSecret();
206
+ context = "remember:global";
207
+ break;
208
+ case "custom":
209
+ ikm = source.key;
210
+ context = `remember:custom:${source.key}`;
211
+ break;
212
+ }
213
+ const ikmBytes = textEncoder.encode(ikm);
214
+ const info = textEncoder.encode(context);
215
+ return hkdfSha256(ikmBytes, salt, info, 32);
216
+ }
217
+ function getKeySourceForScope(scope, context) {
218
+ switch (scope) {
219
+ case "session":
220
+ return { type: "session", sessionId: context.sessionId };
221
+ case "user":
222
+ return { type: "user", userId: context.userId ?? "anonymous" };
223
+ case "tool":
224
+ return {
225
+ type: "tool",
226
+ toolName: context.toolName ?? "unknown",
227
+ sessionId: context.sessionId
228
+ };
229
+ case "global":
230
+ return { type: "global" };
231
+ }
232
+ }
233
+ var textDecoder = new TextDecoder();
234
+ async function encryptValue(data, keySource) {
235
+ const key = await deriveEncryptionKey(keySource);
236
+ const plaintext = textEncoder.encode(JSON.stringify(data));
237
+ const iv = randomBytes2(12);
238
+ const { ciphertext, tag } = encryptAesGcm(key, plaintext, iv);
239
+ return {
240
+ alg: "A256GCM",
241
+ iv: base64urlEncode2(iv),
242
+ tag: base64urlEncode2(tag),
243
+ data: base64urlEncode2(ciphertext)
244
+ };
245
+ }
246
+ async function decryptValue(blob, keySource) {
247
+ try {
248
+ const key = await deriveEncryptionKey(keySource);
249
+ const iv = base64urlDecode(blob.iv);
250
+ const tag = base64urlDecode(blob.tag);
251
+ const ciphertext = base64urlDecode(blob.data);
252
+ const decrypted = decryptAesGcm(key, ciphertext, iv, tag);
253
+ return JSON.parse(textDecoder.decode(decrypted));
254
+ } catch {
255
+ return null;
256
+ }
257
+ }
258
+ function serializeBlob(blob) {
259
+ return JSON.stringify(blob);
260
+ }
261
+ function deserializeBlob(str) {
262
+ try {
263
+ const parsed = JSON.parse(str);
264
+ if (parsed && typeof parsed === "object" && parsed.alg === "A256GCM" && typeof parsed.iv === "string" && typeof parsed.tag === "string" && typeof parsed.data === "string") {
265
+ return parsed;
182
266
  }
183
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
184
- return new RegExp(`^${escaped}$`);
267
+ return null;
268
+ } catch {
269
+ return null;
270
+ }
271
+ }
272
+ async function encryptAndSerialize(data, keySource) {
273
+ const blob = await encryptValue(data, keySource);
274
+ return serializeBlob(blob);
275
+ }
276
+ async function deserializeAndDecrypt(str, keySource) {
277
+ const blob = deserializeBlob(str);
278
+ if (!blob) return null;
279
+ return decryptValue(blob, keySource);
280
+ }
281
+
282
+ // plugins/plugin-remember/src/remember.errors.ts
283
+ var RememberIdentityError = class extends Error {
284
+ name = "RememberIdentityError";
285
+ constructor(message) {
286
+ super(message);
185
287
  }
186
288
  };
187
- // ReDoS protection constants
188
- // Pattern length allows for long prefixes (e.g., remember:session:{uuid}:user:*)
189
- __publicField(RememberMemoryProvider, "MAX_PATTERN_LENGTH", 500);
190
- __publicField(RememberMemoryProvider, "MAX_WILDCARDS", 20);
191
- RememberMemoryProvider = __decorateClass([
192
- Provider({
193
- name: "provider:remember:memory",
194
- description: "In-memory storage provider for RememberPlugin",
195
- scope: ProviderScope.GLOBAL
196
- })
197
- ], RememberMemoryProvider);
198
289
 
199
- // plugins/plugin-remember/src/providers/remember-redis.provider.ts
200
- import Redis from "ioredis";
201
- import { Provider as Provider2, ProviderScope as ProviderScope2 } from "@frontmcp/sdk";
202
- var RememberRedisProvider = class {
203
- client;
204
- /**
205
- * Prefix prepended to all Redis keys.
206
- * Include any separator (e.g., "myapp:" or "user:123:") as part of the prefix.
207
- */
208
- keyPrefix;
209
- /** True if this provider created the client (and should close it), false if externally provided */
210
- ownsClient;
211
- constructor(options) {
212
- this.keyPrefix = options.keyPrefix ?? "";
213
- if (options.type === "redis-client") {
214
- this.client = options.client;
215
- this.ownsClient = false;
216
- return;
217
- }
218
- this.ownsClient = true;
219
- this.client = new Redis({
220
- lazyConnect: false,
221
- maxRetriesPerRequest: 3,
222
- ...options.config
223
- });
224
- this.client.on("connect", () => {
225
- if (process.env["DEBUG"] === "true") {
226
- console.log("[RememberPlugin:Redis] Connected");
227
- }
228
- });
229
- this.client.on("error", (err) => {
230
- console.error("[RememberPlugin:Redis] Error:", err.message);
231
- });
290
+ // plugins/plugin-remember/src/remember.legacy-purge.ts
291
+ var LEGACY_SCOPES = ["session", "tool", "user"];
292
+ var DEFAULT_LEGACY_PURGE_DELAY_MS = 864e5;
293
+ var LAYOUT_MARKER_KEY = "__layout__";
294
+ var LAYOUT_VERSION = 2;
295
+ var MAX_TIMER_MS = 2147483647;
296
+ var scheduled = /* @__PURE__ */ new WeakSet();
297
+ function parseFirstSeenAt(raw) {
298
+ if (!raw) return void 0;
299
+ try {
300
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
301
+ const firstSeenAt = parsed?.firstSeenAt;
302
+ return typeof firstSeenAt === "number" && Number.isFinite(firstSeenAt) ? firstSeenAt : void 0;
303
+ } catch {
304
+ return void 0;
232
305
  }
233
- /**
234
- * Store a value with optional TTL.
235
- *
236
- * @param key - The key to store under
237
- * @param value - The value to store (must not be undefined)
238
- * @param ttlSeconds - Optional TTL in seconds (must be positive integer if provided)
239
- * @throws Error if value is undefined or ttlSeconds is invalid
240
- */
241
- async setValue(key, value, ttlSeconds) {
242
- if (value === void 0) {
243
- throw new Error("Cannot store undefined value. Use null or delete the key instead.");
306
+ }
307
+ async function readLayoutFirstSeenAt(store, keyPrefix, logger) {
308
+ const key = `${keyPrefix}${LAYOUT_MARKER_KEY}`;
309
+ const standDown = (reason, extra = {}) => {
310
+ logger?.debug?.(`remember: ${reason}, legacy purge stood down`, { key, ...extra });
311
+ return void 0;
312
+ };
313
+ try {
314
+ const existing = await store.getValue(key);
315
+ if (existing) {
316
+ return parseFirstSeenAt(existing) ?? standDown("layout marker is unreadable");
244
317
  }
245
- if (ttlSeconds !== void 0) {
246
- if (typeof ttlSeconds !== "number" || !Number.isFinite(ttlSeconds)) {
247
- throw new Error(`Invalid TTL: expected a number, got ${typeof ttlSeconds}`);
248
- }
249
- if (ttlSeconds <= 0) {
250
- throw new Error(`Invalid TTL: must be positive, got ${ttlSeconds}`);
251
- }
252
- if (!Number.isInteger(ttlSeconds)) {
253
- throw new Error(`Invalid TTL: must be an integer, got ${ttlSeconds}`);
254
- }
318
+ const marker = { version: LAYOUT_VERSION, firstSeenAt: Date.now() };
319
+ const serialized = JSON.stringify(marker);
320
+ if (!store.setIfAbsent) {
321
+ await store.setValue(key, serialized);
322
+ return marker.firstSeenAt;
255
323
  }
256
- const fullKey = this.keyPrefix + key;
257
- const strValue = typeof value === "string" ? value : JSON.stringify(value);
258
- if (ttlSeconds !== void 0 && ttlSeconds > 0) {
259
- await this.client.set(fullKey, strValue, "EX", ttlSeconds);
260
- } else {
261
- await this.client.set(fullKey, strValue);
324
+ if (await store.setIfAbsent(key, serialized)) {
325
+ return marker.firstSeenAt;
262
326
  }
327
+ const winner = await store.getValue(key);
328
+ return parseFirstSeenAt(winner) ?? standDown("layout marker vanished after a lost race");
329
+ } catch (error) {
330
+ return standDown("could not establish the layout marker", {
331
+ error: error instanceof Error ? error.message : String(error)
332
+ });
263
333
  }
264
- /**
265
- * Retrieve a value by key.
266
- *
267
- * Returns the parsed JSON value if successful, or `defaultValue` if:
268
- * - The key does not exist
269
- * - The stored value is not valid JSON (malformed or legacy data)
270
- *
271
- * @param key - The key to retrieve
272
- * @param defaultValue - Value to return if key doesn't exist or parsing fails
273
- * @returns The parsed value as T, or defaultValue/undefined
274
- */
275
- async getValue(key, defaultValue) {
276
- const fullKey = this.keyPrefix + key;
277
- const raw = await this.client.get(fullKey);
278
- if (raw === null) return defaultValue;
334
+ }
335
+ async function purgeLegacyRememberEntries(store, keyPrefix, logger) {
336
+ let deleted = 0;
337
+ for (const scope of LEGACY_SCOPES) {
338
+ const pattern = `${keyPrefix}${scope}:*`;
339
+ let keys;
279
340
  try {
280
- return JSON.parse(raw);
281
- } catch {
282
- return defaultValue;
341
+ keys = await store.keys(pattern);
342
+ } catch (error) {
343
+ logger?.debug?.("remember: legacy purge skipped, store could not list keys", {
344
+ pattern,
345
+ error: error instanceof Error ? error.message : String(error)
346
+ });
347
+ continue;
283
348
  }
284
- }
285
- /**
286
- * Delete a key.
287
- */
288
- async delete(key) {
289
- const fullKey = this.keyPrefix + key;
290
- await this.client.del(fullKey);
291
- }
292
- /**
293
- * Check if a key exists.
294
- */
295
- async exists(key) {
296
- const fullKey = this.keyPrefix + key;
297
- return await this.client.exists(fullKey) === 1;
298
- }
299
- /**
300
- * List keys matching a pattern.
301
- * Uses Redis SCAN for efficient iteration.
302
- */
303
- async keys(pattern) {
304
- const searchPattern = this.keyPrefix + (pattern ?? "*");
305
- const result = [];
306
- let cursor = "0";
307
- do {
308
- const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", searchPattern, "COUNT", 100);
309
- cursor = nextCursor;
310
- for (const key of keys) {
311
- result.push(key.slice(this.keyPrefix.length));
349
+ for (const key of keys) {
350
+ try {
351
+ await store.delete(key);
352
+ deleted += 1;
353
+ } catch {
312
354
  }
313
- } while (cursor !== "0");
314
- return result;
315
- }
316
- /**
317
- * Gracefully close the Redis connection.
318
- * Only closes if this provider owns the client (created it internally).
319
- * Externally-provided clients are left open for the caller to manage.
320
- */
321
- async close() {
322
- if (this.ownsClient) {
323
- await this.client.quit();
324
355
  }
325
356
  }
326
- };
327
- RememberRedisProvider = __decorateClass([
328
- Provider2({
329
- name: "provider:remember:redis",
330
- description: "Redis-based storage provider for RememberPlugin",
331
- scope: ProviderScope2.GLOBAL
332
- })
333
- ], RememberRedisProvider);
357
+ if (deleted > 0) {
358
+ logger?.warn?.(
359
+ `remember: purged ${deleted} entr${deleted === 1 ? "y" : "ies"} left unreadable by the session/tool key-derivation change and the namespace encoding. This is a one-time upgrade step; set skipLegacyPurge to handle the migration yourself.`
360
+ );
361
+ }
362
+ return deleted;
363
+ }
364
+ function scheduleLegacyRememberPurge(store, keyPrefix, options = {}) {
365
+ if (scheduled.has(store)) return;
366
+ scheduled.add(store);
367
+ const delayMs = options.delayMs ?? DEFAULT_LEGACY_PURGE_DELAY_MS;
368
+ const arm = (waitMs) => {
369
+ const timer = setTimeout(
370
+ () => {
371
+ void attempt().catch(() => void 0);
372
+ },
373
+ Math.min(waitMs, MAX_TIMER_MS)
374
+ );
375
+ timer.unref?.();
376
+ };
377
+ const attempt = async () => {
378
+ const firstSeenAt = await readLayoutFirstSeenAt(store, keyPrefix, options.logger);
379
+ if (firstSeenAt === void 0) return;
380
+ const remainingMs = firstSeenAt + delayMs - Date.now();
381
+ if (remainingMs > 0) {
382
+ arm(remainingMs);
383
+ return;
384
+ }
385
+ await purgeLegacyRememberEntries(store, keyPrefix, options.logger);
386
+ };
387
+ void attempt().catch(() => void 0);
388
+ }
334
389
 
335
- // plugins/plugin-remember/src/providers/remember-vercel-kv.provider.ts
336
- import { Provider as Provider3, ProviderScope as ProviderScope3 } from "@frontmcp/sdk";
337
- var RememberVercelKvProvider = class {
338
- kv;
390
+ // plugins/plugin-remember/src/providers/remember-accessor.provider.ts
391
+ var STORAGE_LAYOUT_VERSION = "v2";
392
+ function encodeKeyPart(value) {
393
+ return encodeURIComponent(value);
394
+ }
395
+ var RememberAccessor = class {
396
+ store;
397
+ ctx;
398
+ config;
339
399
  keyPrefix;
340
- defaultTTL;
341
- constructor(options = {}) {
342
- const vercelKv = __require("@vercel/kv");
343
- const hasUrl = options.url !== void 0;
344
- const hasToken = options.token !== void 0;
345
- if (hasUrl !== hasToken) {
346
- throw new Error(
347
- `RememberVercelKvProvider: Both 'url' and 'token' must be provided together, or neither. Received: url=${hasUrl ? "provided" : "missing"}, token=${hasToken ? "provided" : "missing"}`
348
- );
349
- }
350
- if (options.url && options.token) {
351
- this.kv = vercelKv.createClient({
352
- url: options.url,
353
- token: options.token
354
- });
355
- } else {
356
- this.kv = vercelKv.kv;
400
+ encryptionEnabled;
401
+ constructor(store, ctx, config, logger) {
402
+ this.store = store;
403
+ this.ctx = ctx;
404
+ this.config = config;
405
+ this.keyPrefix = config.keyPrefix ?? "remember:";
406
+ this.encryptionEnabled = config.encryption?.enabled !== false;
407
+ if (!config.skipLegacyPurge) {
408
+ scheduleLegacyRememberPurge(store, this.keyPrefix, { delayMs: config.legacyPurgeDelayMs, logger });
357
409
  }
358
- this.keyPrefix = options.keyPrefix ?? "remember:";
359
- this.defaultTTL = options.defaultTTL;
360
410
  }
361
- prefixKey(key) {
362
- return `${this.keyPrefix}${key}`;
411
+ // ─────────────────────────────────────────────────────────────────────────────
412
+ // Public API
413
+ // ─────────────────────────────────────────────────────────────────────────────
414
+ /**
415
+ * Store a value in memory.
416
+ *
417
+ * @param key - The key to store under
418
+ * @param value - The value to store (any JSON-serializable data)
419
+ * @param options - Storage options (scope, ttl, brand, metadata)
420
+ */
421
+ async set(key, value, options = {}) {
422
+ const scope = options.scope ?? "session";
423
+ const storageKey = this.buildStorageKey(key, scope);
424
+ const entry = {
425
+ value,
426
+ brand: options.brand,
427
+ createdAt: Date.now(),
428
+ updatedAt: Date.now(),
429
+ expiresAt: options.ttl ? Date.now() + options.ttl * 1e3 : void 0,
430
+ metadata: options.metadata
431
+ };
432
+ const serialized = this.encryptionEnabled ? await encryptAndSerialize(entry, this.getKeySource(scope)) : JSON.stringify(entry);
433
+ await this.store.setValue(storageKey, serialized, options.ttl);
363
434
  }
364
435
  /**
365
- * Store a value with optional TTL.
436
+ * Retrieve a value from memory.
437
+ *
438
+ * @param key - The key to retrieve
439
+ * @param options - Retrieval options (scope, defaultValue)
440
+ * @returns The stored value or defaultValue if not found
366
441
  */
367
- async setValue(key, value, ttlSeconds) {
368
- const fullKey = this.prefixKey(key);
369
- const strValue = typeof value === "string" ? value : JSON.stringify(value);
370
- const ttl = ttlSeconds ?? this.defaultTTL;
371
- if (ttl && ttl > 0) {
372
- await this.kv.set(fullKey, strValue, { ex: ttl });
373
- } else {
374
- await this.kv.set(fullKey, strValue);
442
+ async get(key, options = {}) {
443
+ const scope = options.scope ?? "session";
444
+ const storageKey = this.buildStorageKey(key, scope);
445
+ const raw = await this.store.getValue(storageKey);
446
+ if (!raw) return options.defaultValue;
447
+ const entry = this.encryptionEnabled ? await deserializeAndDecrypt(raw, this.getKeySource(scope)) : this.parseEntry(raw);
448
+ if (!entry) return options.defaultValue;
449
+ if (entry.expiresAt && Date.now() > entry.expiresAt) {
450
+ await this.forget(key, { scope });
451
+ return options.defaultValue;
375
452
  }
453
+ return entry.value;
376
454
  }
377
455
  /**
378
- * Retrieve a value by key.
456
+ * Get the full entry including metadata.
457
+ *
458
+ * @param key - The key to retrieve
459
+ * @param options - Retrieval options (scope)
460
+ * @returns The full entry or undefined if not found
379
461
  */
380
- async getValue(key, defaultValue) {
381
- const fullKey = this.prefixKey(key);
382
- const raw = await this.kv.get(fullKey);
383
- if (raw === null || raw === void 0) return defaultValue;
384
- if (typeof raw === "string") {
385
- try {
386
- return JSON.parse(raw);
387
- } catch {
388
- return raw;
389
- }
462
+ async getEntry(key, options = {}) {
463
+ const scope = options.scope ?? "session";
464
+ const storageKey = this.buildStorageKey(key, scope);
465
+ const raw = await this.store.getValue(storageKey);
466
+ if (!raw) return void 0;
467
+ const entry = this.encryptionEnabled ? await deserializeAndDecrypt(raw, this.getKeySource(scope)) : this.parseEntry(raw);
468
+ if (!entry) return void 0;
469
+ if (entry.expiresAt && Date.now() > entry.expiresAt) {
470
+ await this.forget(key, { scope });
471
+ return void 0;
390
472
  }
391
- return raw;
473
+ return entry;
392
474
  }
393
475
  /**
394
- * Delete a key.
476
+ * Forget (delete) a value from memory.
477
+ *
478
+ * @param key - The key to forget
479
+ * @param options - Options (scope)
395
480
  */
396
- async delete(key) {
397
- const fullKey = this.prefixKey(key);
398
- await this.kv.del(fullKey);
481
+ async forget(key, options = {}) {
482
+ const scope = options.scope ?? "session";
483
+ const storageKey = this.buildStorageKey(key, scope);
484
+ await this.store.delete(storageKey);
399
485
  }
400
486
  /**
401
- * Check if a key exists.
487
+ * Check if a key is remembered (exists and not expired).
488
+ *
489
+ * @param key - The key to check
490
+ * @param options - Options (scope)
491
+ * @returns true if the key exists
402
492
  */
403
- async exists(key) {
404
- const fullKey = this.prefixKey(key);
405
- return await this.kv.exists(fullKey) === 1;
493
+ async knows(key, options = {}) {
494
+ const scope = options.scope ?? "session";
495
+ const storageKey = this.buildStorageKey(key, scope);
496
+ return this.store.exists(storageKey);
406
497
  }
407
498
  /**
408
- * List keys matching a pattern.
409
- * Uses SCAN for efficient iteration.
499
+ * List all remembered keys for a scope.
500
+ *
501
+ * @param options - Options (scope, pattern)
502
+ * @returns Array of keys (without the scope prefix)
410
503
  */
411
- async keys(pattern) {
412
- const searchPattern = this.prefixKey(pattern ?? "*");
413
- const result = [];
414
- try {
415
- let cursor = 0;
416
- do {
417
- const [nextCursor, keys] = await this.kv.scan(cursor, {
418
- match: searchPattern,
419
- count: 100
420
- });
421
- cursor = nextCursor;
422
- for (const key of keys) {
423
- result.push(key.slice(this.keyPrefix.length));
424
- }
425
- } while (cursor !== 0);
426
- } catch {
427
- try {
428
- const keys = await this.kv.keys(searchPattern);
429
- for (const key of keys) {
430
- result.push(key.slice(this.keyPrefix.length));
431
- }
432
- } catch {
433
- console.warn("[RememberPlugin:VercelKV] keys() operation not supported");
434
- }
435
- }
436
- return result;
504
+ async list(options = {}) {
505
+ const scope = options.scope ?? "session";
506
+ const scopePrefix = this.buildScopePrefix(scope);
507
+ const fullPattern = scopePrefix + (options.pattern ?? "*");
508
+ const keys = await this.store.keys(fullPattern);
509
+ return keys.map((k) => k.slice(scopePrefix.length));
437
510
  }
438
511
  /**
439
- * Gracefully close the provider.
440
- * No-op for Vercel KV as it uses stateless REST API.
512
+ * Update an existing entry's value while preserving metadata.
513
+ *
514
+ * @param key - The key to update
515
+ * @param value - The new value
516
+ * @param options - Options (scope, ttl)
441
517
  */
442
- async close() {
443
- }
444
- };
445
- RememberVercelKvProvider = __decorateClass([
446
- Provider3({
447
- name: "provider:remember:vercel-kv",
448
- description: "Vercel KV-based storage provider for RememberPlugin",
449
- scope: ProviderScope3.GLOBAL
450
- })
451
- ], RememberVercelKvProvider);
452
-
453
- // plugins/plugin-remember/src/providers/remember-accessor.provider.ts
454
- import { Provider as Provider4, ProviderScope as ProviderScope4 } from "@frontmcp/sdk";
455
-
456
- // plugins/plugin-remember/src/remember.crypto.ts
457
- import {
458
- hkdfSha256,
459
- encryptAesGcm,
460
- decryptAesGcm,
461
- randomBytes as randomBytes2,
462
- base64urlEncode as base64urlEncode2,
463
- base64urlDecode
464
- } from "@frontmcp/utils";
465
-
466
- // plugins/plugin-remember/src/remember.secret-persistence.ts
467
- import * as path from "path";
468
- import { z } from "@frontmcp/lazy-zod";
469
- import { base64urlEncode, mkdir, randomBytes, readFile, rename, unlink, writeFile } from "@frontmcp/utils";
470
- var DEFAULT_SECRET_FILE_PATH = ".frontmcp/remember-secret.json";
471
- var SECRET_BYTES = 32;
472
- var rememberSecretDataSchema = z.object({
473
- secret: z.string().min(32),
474
- // base64url of 32 bytes is ~43 chars
475
- createdAt: z.number().positive().int(),
476
- version: z.literal(1)
477
- });
478
- function validateSecretData(data) {
479
- const result = rememberSecretDataSchema.safeParse(data);
480
- if (!result.success) {
481
- return { valid: false, error: result.error.issues[0]?.message ?? "Invalid secret structure" };
482
- }
483
- const parsed = result.data;
484
- const now = Date.now();
485
- if (parsed.createdAt > now + 6e4) {
486
- return { valid: false, error: "createdAt is in the future" };
487
- }
488
- const hundredYearsMs = 100 * 365 * 24 * 60 * 60 * 1e3;
489
- if (parsed.createdAt < now - hundredYearsMs) {
490
- return { valid: false, error: "createdAt is too old" };
518
+ async update(key, value, options = {}) {
519
+ const scope = options.scope ?? "session";
520
+ const existing = await this.getEntry(key, { scope });
521
+ if (!existing) return false;
522
+ const entry = {
523
+ ...existing,
524
+ value,
525
+ updatedAt: Date.now(),
526
+ expiresAt: options.ttl ? Date.now() + options.ttl * 1e3 : existing.expiresAt
527
+ };
528
+ const storageKey = this.buildStorageKey(key, scope);
529
+ const serialized = this.encryptionEnabled ? await encryptAndSerialize(entry, this.getKeySource(scope)) : JSON.stringify(entry);
530
+ await this.store.setValue(storageKey, serialized, options.ttl);
531
+ return true;
491
532
  }
492
- return { valid: true };
493
- }
494
- function isSecretPersistenceEnabled(options) {
495
- const isProduction = process.env["NODE_ENV"] === "production";
496
- if (isProduction) {
497
- return options?.forceEnable === true;
533
+ // ─────────────────────────────────────────────────────────────────────────────
534
+ // Convenience Methods
535
+ // ─────────────────────────────────────────────────────────────────────────────
536
+ /**
537
+ * Get the session ID from context.
538
+ */
539
+ get sessionId() {
540
+ return this.ctx.sessionId;
498
541
  }
499
- return true;
500
- }
501
- function resolveSecretPath(options) {
502
- const secretPath = options?.secretPath ?? DEFAULT_SECRET_FILE_PATH;
503
- if (path.isAbsolute(secretPath)) {
504
- return secretPath;
542
+ /**
543
+ * Get the user ID from context (if available).
544
+ */
545
+ get userId() {
546
+ const authInfo = this.ctx.authInfo;
547
+ return authInfo?.extra?.["userId"] ?? authInfo?.extra?.["sub"] ?? authInfo?.clientId ?? void 0;
505
548
  }
506
- return path.resolve(process.cwd(), secretPath);
507
- }
508
- async function loadRememberSecret(options) {
509
- if (!isSecretPersistenceEnabled(options)) {
510
- return null;
549
+ // ─────────────────────────────────────────────────────────────────────────────
550
+ // Private Methods
551
+ // ─────────────────────────────────────────────────────────────────────────────
552
+ /**
553
+ * Build the full storage key including scope prefix.
554
+ */
555
+ buildStorageKey(key, scope) {
556
+ return this.buildScopePrefix(scope) + key;
511
557
  }
512
- const secretPath = resolveSecretPath(options);
513
- try {
514
- const content = await readFile(secretPath, "utf8");
515
- const data = JSON.parse(content);
516
- const validation = validateSecretData(data);
517
- if (!validation.valid) {
518
- console.warn(
519
- `[RememberSecretPersistence] Invalid secret file format at ${secretPath}: ${validation.error}, will regenerate`
520
- );
521
- return null;
558
+ /**
559
+ * The identity that namespaces session- and tool-scoped storage
560
+ * (GHSA-225p-f8jh-f3rh).
561
+ *
562
+ * In stateless mode the transport injects the literal session id `__stateless__` into every
563
+ * request, so using it as a namespace put every client's memory under the same keys. A
564
+ * stateless request carries no session identity: the authenticated principal is the only
565
+ * per-client identity available, and where there is none the request has no business
566
+ * reading or writing per-client memory at all.
567
+ */
568
+ resolveSessionIdentity() {
569
+ const sessionId = this.ctx.sessionId;
570
+ if (sessionId !== STATELESS_SESSION_ID) {
571
+ return sessionId;
522
572
  }
523
- return data;
524
- } catch (error) {
525
- if (error.code === "ENOENT") {
526
- return null;
573
+ const userId = this.userId;
574
+ if (userId) {
575
+ return `stateless-user:${userId}`;
527
576
  }
528
- console.warn(`[RememberSecretPersistence] Failed to load secret from ${secretPath}: ${error.message}`);
529
- return null;
577
+ throw new RememberIdentityError(
578
+ `Remember cannot use session or tool scope for an unauthenticated stateless request: every such request shares the session id "${STATELESS_SESSION_ID}", so the data would be shared across all clients. Authenticate the request, use a stateful transport, or choose the 'global' scope if the data really is shared.`
579
+ );
530
580
  }
531
- }
532
- async function saveRememberSecret(secretData, options) {
533
- if (!isSecretPersistenceEnabled(options)) {
534
- return true;
581
+ /**
582
+ * Build the scope-specific prefix.
583
+ *
584
+ * Every variable component is encoded, because the separator is also a legal character in
585
+ * a user id, a tool name and a memory key. Left raw, identity `a` with key `b:c` produces
586
+ * the same storage key as identity `a:b` with key `c` — one client reading or overwriting
587
+ * another's value through nothing more exotic than a colon in a name.
588
+ */
589
+ buildScopePrefix(scope) {
590
+ switch (scope) {
591
+ case "session":
592
+ return `${this.keyPrefix}${STORAGE_LAYOUT_VERSION}:session:${encodeKeyPart(this.resolveSessionIdentity())}:`;
593
+ case "user": {
594
+ const userId = this.userId;
595
+ if (!userId) {
596
+ throw new RememberIdentityError(
597
+ "Remember cannot use user scope without an authenticated user: all unauthenticated callers would share one namespace. Authenticate the request or use the 'global' scope."
598
+ );
599
+ }
600
+ return `${this.keyPrefix}${STORAGE_LAYOUT_VERSION}:user:${encodeKeyPart(userId)}:`;
601
+ }
602
+ case "tool": {
603
+ const toolName = this.ctx.flow?.name ?? "unknown";
604
+ return `${this.keyPrefix}${STORAGE_LAYOUT_VERSION}:tool:${encodeKeyPart(toolName)}:${encodeKeyPart(this.resolveSessionIdentity())}:`;
605
+ }
606
+ case "global":
607
+ return `${this.keyPrefix}global:`;
608
+ }
535
609
  }
536
- const secretPath = resolveSecretPath(options);
537
- const dir = path.dirname(secretPath);
538
- const tempPath = `${secretPath}.tmp.${Date.now()}.${base64urlEncode(randomBytes(8))}`;
539
- try {
540
- await mkdir(dir, { recursive: true, mode: 448 });
541
- const content = JSON.stringify(secretData, null, 2);
542
- await writeFile(tempPath, content, { mode: 384 });
543
- await rename(tempPath, secretPath);
544
- return true;
545
- } catch (error) {
546
- console.error(`[RememberSecretPersistence] Failed to save secret to ${secretPath}: ${error.message}`);
610
+ /**
611
+ * Get the encryption key source for a scope.
612
+ */
613
+ getKeySource(scope) {
614
+ const needsSessionIdentity = scope === "session" || scope === "tool";
615
+ return getKeySourceForScope(scope, {
616
+ sessionId: needsSessionIdentity ? this.resolveSessionIdentity() : this.ctx.sessionId,
617
+ userId: this.userId,
618
+ toolName: this.ctx.flow?.name
619
+ });
620
+ }
621
+ /**
622
+ * Parse a JSON entry (when encryption is disabled).
623
+ */
624
+ parseEntry(raw) {
547
625
  try {
548
- await unlink(tempPath);
626
+ return JSON.parse(raw);
549
627
  } catch {
628
+ return null;
550
629
  }
551
- return false;
552
630
  }
631
+ };
632
+ RememberAccessor = __decorateClass([
633
+ Provider({
634
+ name: "provider:remember:accessor",
635
+ description: "Context-scoped accessor for RememberPlugin storage",
636
+ scope: ProviderScope.CONTEXT
637
+ })
638
+ ], RememberAccessor);
639
+ function createRememberAccessor(store, ctx, config, logger) {
640
+ return new RememberAccessor(store, ctx, config, logger);
553
641
  }
554
- var cachedSecret = null;
555
- var secretGenerationPromise = null;
556
- function generateSecret() {
557
- return base64urlEncode(randomBytes(SECRET_BYTES));
558
- }
559
- async function getOrCreatePersistedSecret(options) {
560
- if (cachedSecret) {
561
- return cachedSecret;
562
- }
563
- if (secretGenerationPromise) {
564
- return secretGenerationPromise;
642
+
643
+ // plugins/plugin-remember/src/providers/remember-memory.provider.ts
644
+ import { Provider as Provider2, ProviderScope as ProviderScope2 } from "@frontmcp/sdk";
645
+ var MAX_TIMEOUT_MS = 2 ** 31 - 1;
646
+ var RememberMemoryProvider = class {
647
+ memory = /* @__PURE__ */ new Map();
648
+ sweeper;
649
+ constructor(sweepIntervalSeconds = 60) {
650
+ this.sweeper = setInterval(() => this.sweep(), sweepIntervalSeconds * 1e3);
651
+ this.sweeper.unref?.();
565
652
  }
566
- secretGenerationPromise = (async () => {
567
- try {
568
- const loaded = await loadRememberSecret(options);
569
- if (loaded) {
570
- cachedSecret = loaded.secret;
571
- return cachedSecret;
572
- }
573
- const newSecret = generateSecret();
574
- const secretData = {
575
- secret: newSecret,
576
- createdAt: Date.now(),
577
- version: 1
578
- };
579
- if (isSecretPersistenceEnabled(options)) {
580
- await saveRememberSecret(secretData, options);
653
+ /**
654
+ * Store a value with optional TTL.
655
+ * Always JSON-serializes the value for consistent storage/retrieval.
656
+ */
657
+ async setValue(key, value, ttlSeconds) {
658
+ const strValue = JSON.stringify(value);
659
+ const existing = this.memory.get(key);
660
+ if (existing?.timeout) clearTimeout(existing.timeout);
661
+ const entry = { value: strValue };
662
+ if (ttlSeconds && ttlSeconds > 0) {
663
+ const ttlMs = ttlSeconds * 1e3;
664
+ entry.expiresAt = Date.now() + ttlMs;
665
+ if (ttlMs <= MAX_TIMEOUT_MS) {
666
+ entry.timeout = setTimeout(() => {
667
+ const e = this.memory.get(key);
668
+ if (e && e.expiresAt && e.expiresAt <= Date.now()) {
669
+ this.memory.delete(key);
670
+ }
671
+ }, ttlMs);
672
+ entry.timeout.unref?.();
581
673
  }
582
- cachedSecret = newSecret;
583
- return cachedSecret;
584
- } finally {
585
- secretGenerationPromise = null;
586
674
  }
587
- })();
588
- return secretGenerationPromise;
589
- }
590
-
591
- // plugins/plugin-remember/src/remember.crypto.ts
592
- async function getBaseSecret() {
593
- const envSecret = process.env["REMEMBER_SECRET"] || process.env["MCP_MEMORY_SECRET"] || process.env["MCP_SESSION_SECRET"];
594
- if (envSecret) {
595
- return envSecret;
675
+ this.memory.set(key, entry);
596
676
  }
597
- return getOrCreatePersistedSecret();
598
- }
599
- var textEncoder = new TextEncoder();
600
- async function deriveEncryptionKey(source) {
601
- const salt = textEncoder.encode("remember-plugin-v1");
602
- let ikm;
603
- let context;
604
- switch (source.type) {
605
- case "session":
606
- ikm = source.sessionId;
607
- context = `remember:session:${source.sessionId}`;
608
- break;
609
- case "tool":
610
- ikm = source.sessionId;
611
- context = `remember:tool:${source.toolName}:${source.sessionId}`;
612
- break;
613
- case "user":
614
- ikm = await getBaseSecret() + source.userId;
615
- context = `remember:user:${source.userId}`;
616
- break;
617
- case "global":
618
- ikm = await getBaseSecret();
619
- context = "remember:global";
620
- break;
621
- case "custom":
622
- ikm = source.key;
623
- context = `remember:custom:${source.key}`;
624
- break;
677
+ /**
678
+ * Store a value only if the key is absent.
679
+ *
680
+ * A single-threaded runtime makes the check-then-set indivisible; an expired entry counts as
681
+ * absent, matching what `getValue` would report.
682
+ */
683
+ async setIfAbsent(key, value, ttlSeconds) {
684
+ const existing = this.memory.get(key);
685
+ if (existing && !this.isExpired(existing)) return false;
686
+ await this.setValue(key, value, ttlSeconds);
687
+ return true;
625
688
  }
626
- const ikmBytes = textEncoder.encode(ikm);
627
- const info = textEncoder.encode(context);
628
- return hkdfSha256(ikmBytes, salt, info, 32);
629
- }
630
- function getKeySourceForScope(scope, context) {
631
- switch (scope) {
632
- case "session":
633
- return { type: "session", sessionId: context.sessionId };
634
- case "user":
635
- return { type: "user", userId: context.userId ?? "anonymous" };
636
- case "tool":
637
- return {
638
- type: "tool",
639
- toolName: context.toolName ?? "unknown",
640
- sessionId: context.sessionId
641
- };
642
- case "global":
643
- return { type: "global" };
689
+ /**
690
+ * Retrieve a value by key.
691
+ * JSON-parses the stored value to match the setValue serialization.
692
+ */
693
+ async getValue(key, defaultValue) {
694
+ const entry = this.memory.get(key);
695
+ if (!entry) return defaultValue;
696
+ if (this.isExpired(entry)) {
697
+ await this.delete(key);
698
+ return defaultValue;
699
+ }
700
+ try {
701
+ return JSON.parse(entry.value);
702
+ } catch {
703
+ return entry.value;
704
+ }
705
+ }
706
+ /**
707
+ * Delete a key.
708
+ */
709
+ async delete(key) {
710
+ const entry = this.memory.get(key);
711
+ if (entry?.timeout) clearTimeout(entry.timeout);
712
+ this.memory.delete(key);
644
713
  }
645
- }
646
- var textDecoder = new TextDecoder();
647
- async function encryptValue(data, keySource) {
648
- const key = await deriveEncryptionKey(keySource);
649
- const plaintext = textEncoder.encode(JSON.stringify(data));
650
- const iv = randomBytes2(12);
651
- const { ciphertext, tag } = encryptAesGcm(key, plaintext, iv);
652
- return {
653
- alg: "A256GCM",
654
- iv: base64urlEncode2(iv),
655
- tag: base64urlEncode2(tag),
656
- data: base64urlEncode2(ciphertext)
657
- };
658
- }
659
- async function decryptValue(blob, keySource) {
660
- try {
661
- const key = await deriveEncryptionKey(keySource);
662
- const iv = base64urlDecode(blob.iv);
663
- const tag = base64urlDecode(blob.tag);
664
- const ciphertext = base64urlDecode(blob.data);
665
- const decrypted = decryptAesGcm(key, ciphertext, iv, tag);
666
- return JSON.parse(textDecoder.decode(decrypted));
667
- } catch {
668
- return null;
714
+ /**
715
+ * Check if a key exists (and is not expired).
716
+ */
717
+ async exists(key) {
718
+ const entry = this.memory.get(key);
719
+ if (!entry) return false;
720
+ if (this.isExpired(entry)) {
721
+ await this.delete(key);
722
+ return false;
723
+ }
724
+ return true;
669
725
  }
670
- }
671
- function serializeBlob(blob) {
672
- return JSON.stringify(blob);
673
- }
674
- function deserializeBlob(str) {
675
- try {
676
- const parsed = JSON.parse(str);
677
- if (parsed && typeof parsed === "object" && parsed.alg === "A256GCM" && typeof parsed.iv === "string" && typeof parsed.tag === "string" && typeof parsed.data === "string") {
678
- return parsed;
726
+ /**
727
+ * List keys matching a glob-style pattern.
728
+ * Supports * and ? wildcards.
729
+ */
730
+ async keys(pattern) {
731
+ const result = [];
732
+ const regex = pattern ? this.patternToRegex(pattern) : null;
733
+ const expiredKeys = [];
734
+ for (const [key, entry] of this.memory) {
735
+ if (this.isExpired(entry)) {
736
+ expiredKeys.push(key);
737
+ }
679
738
  }
680
- return null;
681
- } catch {
682
- return null;
739
+ for (const key of expiredKeys) {
740
+ await this.delete(key);
741
+ }
742
+ for (const [key, entry] of this.memory) {
743
+ if (this.isExpired(entry)) {
744
+ continue;
745
+ }
746
+ if (regex && !regex.test(key)) {
747
+ continue;
748
+ }
749
+ result.push(key);
750
+ }
751
+ return result;
683
752
  }
684
- }
685
- async function encryptAndSerialize(data, keySource) {
686
- const blob = await encryptValue(data, keySource);
687
- return serializeBlob(blob);
688
- }
689
- async function deserializeAndDecrypt(str, keySource) {
690
- const blob = deserializeBlob(str);
691
- if (!blob) return null;
692
- return decryptValue(blob, keySource);
693
- }
694
-
695
- // plugins/plugin-remember/src/providers/remember-accessor.provider.ts
696
- var RememberAccessor = class {
697
- store;
698
- ctx;
699
- config;
700
- keyPrefix;
701
- encryptionEnabled;
702
- constructor(store, ctx, config) {
703
- this.store = store;
704
- this.ctx = ctx;
705
- this.config = config;
706
- this.keyPrefix = config.keyPrefix ?? "remember:";
707
- this.encryptionEnabled = config.encryption?.enabled !== false;
753
+ /**
754
+ * Gracefully close the provider.
755
+ */
756
+ async close() {
757
+ if (this.sweeper) clearInterval(this.sweeper);
758
+ for (const [, entry] of this.memory) {
759
+ if (entry.timeout) clearTimeout(entry.timeout);
760
+ }
761
+ this.memory.clear();
708
762
  }
709
763
  // ─────────────────────────────────────────────────────────────────────────────
710
- // Public API
764
+ // Private Methods
711
765
  // ─────────────────────────────────────────────────────────────────────────────
766
+ isExpired(entry) {
767
+ return entry.expiresAt !== void 0 && entry.expiresAt <= Date.now();
768
+ }
712
769
  /**
713
- * Store a value in memory.
714
- *
715
- * @param key - The key to store under
716
- * @param value - The value to store (any JSON-serializable data)
717
- * @param options - Storage options (scope, ttl, brand, metadata)
770
+ * Periodically remove expired keys to keep memory tidy.
718
771
  */
719
- async set(key, value, options = {}) {
720
- const scope = options.scope ?? "session";
721
- const storageKey = this.buildStorageKey(key, scope);
722
- const entry = {
723
- value,
724
- brand: options.brand,
725
- createdAt: Date.now(),
726
- updatedAt: Date.now(),
727
- expiresAt: options.ttl ? Date.now() + options.ttl * 1e3 : void 0,
728
- metadata: options.metadata
729
- };
730
- const serialized = this.encryptionEnabled ? await encryptAndSerialize(entry, this.getKeySource(scope)) : JSON.stringify(entry);
731
- await this.store.setValue(storageKey, serialized, options.ttl);
772
+ sweep() {
773
+ const now = Date.now();
774
+ for (const [key, entry] of this.memory) {
775
+ if (entry.expiresAt !== void 0 && entry.expiresAt <= now) {
776
+ if (entry.timeout) clearTimeout(entry.timeout);
777
+ this.memory.delete(key);
778
+ }
779
+ }
732
780
  }
733
781
  /**
734
- * Retrieve a value from memory.
782
+ * Convert a glob-style pattern to a RegExp.
783
+ * * matches any sequence of characters
784
+ * ? matches any single character
735
785
  *
736
- * @param key - The key to retrieve
737
- * @param options - Retrieval options (scope, defaultValue)
738
- * @returns The stored value or defaultValue if not found
786
+ * Includes ReDoS protection: validates pattern length and wildcard count.
739
787
  */
740
- async get(key, options = {}) {
741
- const scope = options.scope ?? "session";
742
- const storageKey = this.buildStorageKey(key, scope);
743
- const raw = await this.store.getValue(storageKey);
744
- if (!raw) return options.defaultValue;
745
- const entry = this.encryptionEnabled ? await deserializeAndDecrypt(raw, this.getKeySource(scope)) : this.parseEntry(raw);
746
- if (!entry) return options.defaultValue;
747
- if (entry.expiresAt && Date.now() > entry.expiresAt) {
748
- await this.forget(key, { scope });
749
- return options.defaultValue;
788
+ patternToRegex(pattern) {
789
+ if (pattern.length > RememberMemoryProvider.MAX_PATTERN_LENGTH) {
790
+ throw new Error(`Pattern too long: ${pattern.length} chars (max ${RememberMemoryProvider.MAX_PATTERN_LENGTH})`);
750
791
  }
751
- return entry.value;
792
+ const wildcardCount = (pattern.match(/[*?]/g) || []).length;
793
+ if (wildcardCount > RememberMemoryProvider.MAX_WILDCARDS) {
794
+ throw new Error(`Too many wildcards in pattern: ${wildcardCount} (max ${RememberMemoryProvider.MAX_WILDCARDS})`);
795
+ }
796
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
797
+ return new RegExp(`^${escaped}$`);
752
798
  }
799
+ };
800
+ // ReDoS protection constants
801
+ // Pattern length allows for long prefixes (e.g., remember:session:{uuid}:user:*)
802
+ __publicField(RememberMemoryProvider, "MAX_PATTERN_LENGTH", 500);
803
+ __publicField(RememberMemoryProvider, "MAX_WILDCARDS", 20);
804
+ RememberMemoryProvider = __decorateClass([
805
+ Provider2({
806
+ name: "provider:remember:memory",
807
+ description: "In-memory storage provider for RememberPlugin",
808
+ scope: ProviderScope2.GLOBAL
809
+ })
810
+ ], RememberMemoryProvider);
811
+
812
+ // plugins/plugin-remember/src/providers/remember-redis.provider.ts
813
+ import Redis from "ioredis";
814
+ import { Provider as Provider3, ProviderScope as ProviderScope3 } from "@frontmcp/sdk";
815
+ var RememberRedisProvider = class {
816
+ client;
753
817
  /**
754
- * Get the full entry including metadata.
818
+ * Prefix prepended to all Redis keys.
819
+ * Include any separator (e.g., "myapp:" or "user:123:") as part of the prefix.
820
+ */
821
+ keyPrefix;
822
+ /** True if this provider created the client (and should close it), false if externally provided */
823
+ ownsClient;
824
+ constructor(options) {
825
+ this.keyPrefix = options.keyPrefix ?? "";
826
+ if (options.type === "redis-client") {
827
+ this.client = options.client;
828
+ this.ownsClient = false;
829
+ return;
830
+ }
831
+ this.ownsClient = true;
832
+ this.client = new Redis({
833
+ lazyConnect: false,
834
+ maxRetriesPerRequest: 3,
835
+ ...options.config
836
+ });
837
+ this.client.on("connect", () => {
838
+ if (process.env["DEBUG"] === "true") {
839
+ console.log("[RememberPlugin:Redis] Connected");
840
+ }
841
+ });
842
+ this.client.on("error", (err) => {
843
+ console.error("[RememberPlugin:Redis] Error:", err.message);
844
+ });
845
+ }
846
+ /**
847
+ * Store a value with optional TTL.
755
848
  *
756
- * @param key - The key to retrieve
757
- * @param options - Retrieval options (scope)
758
- * @returns The full entry or undefined if not found
849
+ * @param key - The key to store under
850
+ * @param value - The value to store (must not be undefined)
851
+ * @param ttlSeconds - Optional TTL in seconds (must be positive integer if provided)
852
+ * @throws Error if value is undefined or ttlSeconds is invalid
759
853
  */
760
- async getEntry(key, options = {}) {
761
- const scope = options.scope ?? "session";
762
- const storageKey = this.buildStorageKey(key, scope);
763
- const raw = await this.store.getValue(storageKey);
764
- if (!raw) return void 0;
765
- const entry = this.encryptionEnabled ? await deserializeAndDecrypt(raw, this.getKeySource(scope)) : this.parseEntry(raw);
766
- if (!entry) return void 0;
767
- if (entry.expiresAt && Date.now() > entry.expiresAt) {
768
- await this.forget(key, { scope });
769
- return void 0;
854
+ async setValue(key, value, ttlSeconds) {
855
+ if (value === void 0) {
856
+ throw new Error("Cannot store undefined value. Use null or delete the key instead.");
857
+ }
858
+ if (ttlSeconds !== void 0) {
859
+ if (typeof ttlSeconds !== "number" || !Number.isFinite(ttlSeconds)) {
860
+ throw new Error(`Invalid TTL: expected a number, got ${typeof ttlSeconds}`);
861
+ }
862
+ if (ttlSeconds <= 0) {
863
+ throw new Error(`Invalid TTL: must be positive, got ${ttlSeconds}`);
864
+ }
865
+ if (!Number.isInteger(ttlSeconds)) {
866
+ throw new Error(`Invalid TTL: must be an integer, got ${ttlSeconds}`);
867
+ }
868
+ }
869
+ const fullKey = this.keyPrefix + key;
870
+ const strValue = typeof value === "string" ? value : JSON.stringify(value);
871
+ if (ttlSeconds !== void 0 && ttlSeconds > 0) {
872
+ await this.client.set(fullKey, strValue, "EX", ttlSeconds);
873
+ } else {
874
+ await this.client.set(fullKey, strValue);
770
875
  }
771
- return entry;
772
876
  }
773
877
  /**
774
- * Forget (delete) a value from memory.
878
+ * Store a value only if the key is absent, via Redis `SET ... NX`.
775
879
  *
776
- * @param key - The key to forget
777
- * @param options - Options (scope)
880
+ * `set` resolves to `'OK'` when it created the key and `null` when the key already existed,
881
+ * so the whole check-and-write is one round trip and two callers racing cannot both win.
778
882
  */
779
- async forget(key, options = {}) {
780
- const scope = options.scope ?? "session";
781
- const storageKey = this.buildStorageKey(key, scope);
782
- await this.store.delete(storageKey);
883
+ async setIfAbsent(key, value, ttlSeconds) {
884
+ const fullKey = this.keyPrefix + key;
885
+ const strValue = typeof value === "string" ? value : JSON.stringify(value);
886
+ const result = ttlSeconds !== void 0 && ttlSeconds > 0 ? await this.client.set(fullKey, strValue, "EX", ttlSeconds, "NX") : await this.client.set(fullKey, strValue, "NX");
887
+ return result === "OK";
783
888
  }
784
889
  /**
785
- * Check if a key is remembered (exists and not expired).
890
+ * Retrieve a value by key.
786
891
  *
787
- * @param key - The key to check
788
- * @param options - Options (scope)
789
- * @returns true if the key exists
892
+ * Returns the parsed JSON value if successful, or `defaultValue` if:
893
+ * - The key does not exist
894
+ * - The stored value is not valid JSON (malformed or legacy data)
895
+ *
896
+ * @param key - The key to retrieve
897
+ * @param defaultValue - Value to return if key doesn't exist or parsing fails
898
+ * @returns The parsed value as T, or defaultValue/undefined
790
899
  */
791
- async knows(key, options = {}) {
792
- const scope = options.scope ?? "session";
793
- const storageKey = this.buildStorageKey(key, scope);
794
- return this.store.exists(storageKey);
900
+ async getValue(key, defaultValue) {
901
+ const fullKey = this.keyPrefix + key;
902
+ const raw = await this.client.get(fullKey);
903
+ if (raw === null) return defaultValue;
904
+ try {
905
+ return JSON.parse(raw);
906
+ } catch {
907
+ return defaultValue;
908
+ }
795
909
  }
796
910
  /**
797
- * List all remembered keys for a scope.
798
- *
799
- * @param options - Options (scope, pattern)
800
- * @returns Array of keys (without the scope prefix)
911
+ * Delete a key.
801
912
  */
802
- async list(options = {}) {
803
- const scope = options.scope ?? "session";
804
- const scopePrefix = this.buildScopePrefix(scope);
805
- const fullPattern = scopePrefix + (options.pattern ?? "*");
806
- const keys = await this.store.keys(fullPattern);
807
- return keys.map((k) => k.slice(scopePrefix.length));
913
+ async delete(key) {
914
+ const fullKey = this.keyPrefix + key;
915
+ await this.client.del(fullKey);
808
916
  }
809
917
  /**
810
- * Update an existing entry's value while preserving metadata.
811
- *
812
- * @param key - The key to update
813
- * @param value - The new value
814
- * @param options - Options (scope, ttl)
918
+ * Check if a key exists.
815
919
  */
816
- async update(key, value, options = {}) {
817
- const scope = options.scope ?? "session";
818
- const existing = await this.getEntry(key, { scope });
819
- if (!existing) return false;
820
- const entry = {
821
- ...existing,
822
- value,
823
- updatedAt: Date.now(),
824
- expiresAt: options.ttl ? Date.now() + options.ttl * 1e3 : existing.expiresAt
825
- };
826
- const storageKey = this.buildStorageKey(key, scope);
827
- const serialized = this.encryptionEnabled ? await encryptAndSerialize(entry, this.getKeySource(scope)) : JSON.stringify(entry);
828
- await this.store.setValue(storageKey, serialized, options.ttl);
829
- return true;
920
+ async exists(key) {
921
+ const fullKey = this.keyPrefix + key;
922
+ return await this.client.exists(fullKey) === 1;
830
923
  }
831
- // ─────────────────────────────────────────────────────────────────────────────
832
- // Convenience Methods
833
- // ─────────────────────────────────────────────────────────────────────────────
834
924
  /**
835
- * Get the session ID from context.
925
+ * List keys matching a pattern.
926
+ * Uses Redis SCAN for efficient iteration.
836
927
  */
837
- get sessionId() {
838
- return this.ctx.sessionId;
928
+ async keys(pattern) {
929
+ const searchPattern = this.keyPrefix + (pattern ?? "*");
930
+ const result = [];
931
+ let cursor = "0";
932
+ do {
933
+ const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", searchPattern, "COUNT", 100);
934
+ cursor = nextCursor;
935
+ for (const key of keys) {
936
+ result.push(key.slice(this.keyPrefix.length));
937
+ }
938
+ } while (cursor !== "0");
939
+ return result;
839
940
  }
840
941
  /**
841
- * Get the user ID from context (if available).
942
+ * Gracefully close the Redis connection.
943
+ * Only closes if this provider owns the client (created it internally).
944
+ * Externally-provided clients are left open for the caller to manage.
842
945
  */
843
- get userId() {
844
- const authInfo = this.ctx.authInfo;
845
- return authInfo?.extra?.["userId"] ?? authInfo?.extra?.["sub"] ?? authInfo?.clientId ?? void 0;
946
+ async close() {
947
+ if (this.ownsClient) {
948
+ await this.client.quit();
949
+ }
950
+ }
951
+ };
952
+ RememberRedisProvider = __decorateClass([
953
+ Provider3({
954
+ name: "provider:remember:redis",
955
+ description: "Redis-based storage provider for RememberPlugin",
956
+ scope: ProviderScope3.GLOBAL
957
+ })
958
+ ], RememberRedisProvider);
959
+
960
+ // plugins/plugin-remember/src/providers/remember-vercel-kv.provider.ts
961
+ import { Provider as Provider4, ProviderScope as ProviderScope4 } from "@frontmcp/sdk";
962
+ var RememberVercelKvProvider = class {
963
+ kv;
964
+ keyPrefix;
965
+ defaultTTL;
966
+ constructor(options = {}) {
967
+ const vercelKv = __require("@vercel/kv");
968
+ const hasUrl = options.url !== void 0;
969
+ const hasToken = options.token !== void 0;
970
+ if (hasUrl !== hasToken) {
971
+ throw new Error(
972
+ `RememberVercelKvProvider: Both 'url' and 'token' must be provided together, or neither. Received: url=${hasUrl ? "provided" : "missing"}, token=${hasToken ? "provided" : "missing"}`
973
+ );
974
+ }
975
+ if (options.url && options.token) {
976
+ this.kv = vercelKv.createClient({
977
+ url: options.url,
978
+ token: options.token
979
+ });
980
+ } else {
981
+ this.kv = vercelKv.kv;
982
+ }
983
+ this.keyPrefix = options.keyPrefix ?? "remember:";
984
+ this.defaultTTL = options.defaultTTL;
985
+ }
986
+ prefixKey(key) {
987
+ return `${this.keyPrefix}${key}`;
846
988
  }
847
- // ─────────────────────────────────────────────────────────────────────────────
848
- // Private Methods
849
- // ─────────────────────────────────────────────────────────────────────────────
850
989
  /**
851
- * Build the full storage key including scope prefix.
990
+ * Store a value with optional TTL.
852
991
  */
853
- buildStorageKey(key, scope) {
854
- return this.buildScopePrefix(scope) + key;
992
+ async setValue(key, value, ttlSeconds) {
993
+ const fullKey = this.prefixKey(key);
994
+ const strValue = typeof value === "string" ? value : JSON.stringify(value);
995
+ const ttl = ttlSeconds ?? this.defaultTTL;
996
+ if (ttl && ttl > 0) {
997
+ await this.kv.set(fullKey, strValue, { ex: ttl });
998
+ } else {
999
+ await this.kv.set(fullKey, strValue);
1000
+ }
855
1001
  }
856
1002
  /**
857
- * Build the scope-specific prefix.
1003
+ * Store a value only if the key is absent, via the `nx` option.
1004
+ *
1005
+ * Resolves to `'OK'` when it created the key and `null` when the key already existed, so two
1006
+ * callers racing cannot both win.
858
1007
  */
859
- buildScopePrefix(scope) {
860
- switch (scope) {
861
- case "session":
862
- return `${this.keyPrefix}session:${this.ctx.sessionId}:`;
863
- case "user":
864
- return `${this.keyPrefix}user:${this.userId ?? "anonymous"}:`;
865
- case "tool": {
866
- const toolName = this.ctx.flow?.name ?? "unknown";
867
- return `${this.keyPrefix}tool:${toolName}:${this.ctx.sessionId}:`;
1008
+ async setIfAbsent(key, value, ttlSeconds) {
1009
+ const fullKey = this.prefixKey(key);
1010
+ const strValue = typeof value === "string" ? value : JSON.stringify(value);
1011
+ const ttl = ttlSeconds ?? this.defaultTTL;
1012
+ const result = ttl && ttl > 0 ? await this.kv.set(fullKey, strValue, { nx: true, ex: ttl }) : await this.kv.set(fullKey, strValue, { nx: true });
1013
+ return result === "OK";
1014
+ }
1015
+ /**
1016
+ * Retrieve a value by key.
1017
+ */
1018
+ async getValue(key, defaultValue) {
1019
+ const fullKey = this.prefixKey(key);
1020
+ const raw = await this.kv.get(fullKey);
1021
+ if (raw === null || raw === void 0) return defaultValue;
1022
+ if (typeof raw === "string") {
1023
+ try {
1024
+ return JSON.parse(raw);
1025
+ } catch {
1026
+ return raw;
868
1027
  }
869
- case "global":
870
- return `${this.keyPrefix}global:`;
871
1028
  }
1029
+ return raw;
872
1030
  }
873
1031
  /**
874
- * Get the encryption key source for a scope.
1032
+ * Delete a key.
875
1033
  */
876
- getKeySource(scope) {
877
- return getKeySourceForScope(scope, {
878
- sessionId: this.ctx.sessionId,
879
- userId: this.userId,
880
- toolName: this.ctx.flow?.name
881
- });
1034
+ async delete(key) {
1035
+ const fullKey = this.prefixKey(key);
1036
+ await this.kv.del(fullKey);
882
1037
  }
883
1038
  /**
884
- * Parse a JSON entry (when encryption is disabled).
1039
+ * Check if a key exists.
885
1040
  */
886
- parseEntry(raw) {
1041
+ async exists(key) {
1042
+ const fullKey = this.prefixKey(key);
1043
+ return await this.kv.exists(fullKey) === 1;
1044
+ }
1045
+ /**
1046
+ * List keys matching a pattern.
1047
+ * Uses SCAN for efficient iteration.
1048
+ */
1049
+ async keys(pattern) {
1050
+ const searchPattern = this.prefixKey(pattern ?? "*");
1051
+ const result = [];
887
1052
  try {
888
- return JSON.parse(raw);
1053
+ let cursor = 0;
1054
+ do {
1055
+ const [nextCursor, keys] = await this.kv.scan(cursor, {
1056
+ match: searchPattern,
1057
+ count: 100
1058
+ });
1059
+ cursor = nextCursor;
1060
+ for (const key of keys) {
1061
+ result.push(key.slice(this.keyPrefix.length));
1062
+ }
1063
+ } while (cursor !== 0);
889
1064
  } catch {
890
- return null;
1065
+ try {
1066
+ const keys = await this.kv.keys(searchPattern);
1067
+ for (const key of keys) {
1068
+ result.push(key.slice(this.keyPrefix.length));
1069
+ }
1070
+ } catch {
1071
+ console.warn("[RememberPlugin:VercelKV] keys() operation not supported");
1072
+ }
891
1073
  }
1074
+ return result;
1075
+ }
1076
+ /**
1077
+ * Gracefully close the provider.
1078
+ * No-op for Vercel KV as it uses stateless REST API.
1079
+ */
1080
+ async close() {
892
1081
  }
893
1082
  };
894
- RememberAccessor = __decorateClass([
1083
+ RememberVercelKvProvider = __decorateClass([
895
1084
  Provider4({
896
- name: "provider:remember:accessor",
897
- description: "Context-scoped accessor for RememberPlugin storage",
898
- scope: ProviderScope4.CONTEXT
1085
+ name: "provider:remember:vercel-kv",
1086
+ description: "Vercel KV-based storage provider for RememberPlugin",
1087
+ scope: ProviderScope4.GLOBAL
899
1088
  })
900
- ], RememberAccessor);
901
- function createRememberAccessor(store, ctx, config) {
902
- return new RememberAccessor(store, ctx, config);
903
- }
1089
+ ], RememberVercelKvProvider);
1090
+
1091
+ // plugins/plugin-remember/src/remember.symbols.ts
1092
+ var RememberStoreToken = /* @__PURE__ */ Symbol(
1093
+ "plugin:remember:store"
1094
+ );
1095
+ var RememberConfigToken = /* @__PURE__ */ Symbol(
1096
+ "plugin:remember:config"
1097
+ );
1098
+ var RememberAccessorToken = /* @__PURE__ */ Symbol(
1099
+ "plugin:remember:accessor"
1100
+ );
904
1101
 
905
1102
  // plugins/plugin-remember/src/remember.plugin.ts
906
1103
  var RememberPlugin = class extends DynamicPlugin {
@@ -1015,8 +1212,8 @@ __publicField(RememberPlugin, "dynamicProviders", (options) => {
1015
1212
  name: "remember:accessor",
1016
1213
  provide: RememberAccessorToken,
1017
1214
  scope: ProviderScope5.CONTEXT,
1018
- inject: () => [RememberStoreToken, FRONTMCP_CONTEXT2, RememberConfigToken],
1019
- useFactory: (store, ctx, cfg) => createRememberAccessor(store, ctx, cfg)
1215
+ inject: () => [RememberStoreToken, FRONTMCP_CONTEXT, RememberConfigToken, FrontMcpLogger],
1216
+ useFactory: (store, ctx, cfg, logger) => createRememberAccessor(store, ctx, cfg, logger)
1020
1217
  });
1021
1218
  return providers;
1022
1219
  });
@@ -1276,6 +1473,7 @@ export {
1276
1473
  RememberAccessor,
1277
1474
  RememberAccessorToken,
1278
1475
  RememberConfigToken,
1476
+ RememberIdentityError,
1279
1477
  RememberMemoryProvider,
1280
1478
  RememberPlugin,
1281
1479
  RememberRedisProvider,