@frontmcp/plugin-remember 1.7.2 → 1.8.0

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