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