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