layercache 1.2.3 → 1.2.5

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/dist/index.cjs CHANGED
@@ -176,6 +176,7 @@ var CacheNamespace = class _CacheNamespace {
176
176
  * ```
177
177
  */
178
178
  namespace(childPrefix) {
179
+ validateNamespaceKey(childPrefix);
179
180
  return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
180
181
  }
181
182
  qualify(key) {
@@ -305,6 +306,118 @@ function addMap(base, delta) {
305
306
  }
306
307
  return result;
307
308
  }
309
+ function validateNamespaceKey(key) {
310
+ if (key.length === 0) {
311
+ throw new Error("Namespace prefix must not be empty.");
312
+ }
313
+ if (key.length > 256) {
314
+ throw new Error("Namespace prefix must be at most 256 characters.");
315
+ }
316
+ if (/[\u0000-\u001F\u007F]/.test(key)) {
317
+ throw new Error("Namespace prefix contains unsupported control characters.");
318
+ }
319
+ }
320
+
321
+ // src/invalidation/PatternMatcher.ts
322
+ var PatternMatcher = class _PatternMatcher {
323
+ /**
324
+ * Tests whether a glob-style pattern matches a value.
325
+ * Supports `*` (any sequence of characters) and `?` (any single character).
326
+ * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
327
+ * quadratic memory usage on long patterns/keys.
328
+ */
329
+ static matches(pattern, value) {
330
+ return _PatternMatcher.matchLinear(pattern, value);
331
+ }
332
+ /**
333
+ * Linear-time glob matching with O(1) extra memory.
334
+ */
335
+ static matchLinear(pattern, value) {
336
+ let patternIndex = 0;
337
+ let valueIndex = 0;
338
+ let starIndex = -1;
339
+ let backtrackValueIndex = 0;
340
+ while (valueIndex < value.length) {
341
+ const patternChar = pattern[patternIndex];
342
+ const valueChar = value[valueIndex];
343
+ if (patternChar === "*" && patternIndex < pattern.length) {
344
+ starIndex = patternIndex;
345
+ patternIndex += 1;
346
+ backtrackValueIndex = valueIndex;
347
+ continue;
348
+ }
349
+ if (patternChar === "?" || patternChar === valueChar) {
350
+ patternIndex += 1;
351
+ valueIndex += 1;
352
+ continue;
353
+ }
354
+ if (starIndex !== -1) {
355
+ patternIndex = starIndex + 1;
356
+ backtrackValueIndex += 1;
357
+ valueIndex = backtrackValueIndex;
358
+ continue;
359
+ }
360
+ return false;
361
+ }
362
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
363
+ patternIndex += 1;
364
+ }
365
+ return patternIndex === pattern.length;
366
+ }
367
+ };
368
+
369
+ // src/internal/CacheKeyDiscovery.ts
370
+ var CacheKeyDiscovery = class {
371
+ constructor(options) {
372
+ this.options = options;
373
+ }
374
+ options;
375
+ async collectKeysWithPrefix(prefix) {
376
+ const { tagIndex } = this.options;
377
+ const matches = new Set(
378
+ tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
379
+ );
380
+ await Promise.all(
381
+ this.options.layers.map(async (layer) => {
382
+ if (!layer.keys || this.options.shouldSkipLayer(layer)) {
383
+ return;
384
+ }
385
+ try {
386
+ const keys = await layer.keys();
387
+ for (const key of keys) {
388
+ if (key.startsWith(prefix)) {
389
+ matches.add(key);
390
+ }
391
+ }
392
+ } catch (error) {
393
+ await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
394
+ }
395
+ })
396
+ );
397
+ return [...matches];
398
+ }
399
+ async collectKeysMatchingPattern(pattern) {
400
+ const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
401
+ await Promise.all(
402
+ this.options.layers.map(async (layer) => {
403
+ if (!layer.keys || this.options.shouldSkipLayer(layer)) {
404
+ return;
405
+ }
406
+ try {
407
+ const keys = await layer.keys();
408
+ for (const key of keys) {
409
+ if (PatternMatcher.matches(pattern, key)) {
410
+ matches.add(key);
411
+ }
412
+ }
413
+ } catch (error) {
414
+ await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
415
+ }
416
+ })
417
+ );
418
+ return [...matches];
419
+ }
420
+ };
308
421
 
309
422
  // src/internal/CircuitBreakerManager.ts
310
423
  var CircuitBreakerManager = class {
@@ -324,9 +437,7 @@ var CircuitBreakerManager = class {
324
437
  }
325
438
  const now = Date.now();
326
439
  if (state.openUntil <= now) {
327
- state.openUntil = null;
328
- state.failures = 0;
329
- this.breakers.set(key, state);
440
+ this.breakers.delete(key);
330
441
  return;
331
442
  }
332
443
  const remainingMs = state.openUntil - now;
@@ -337,15 +448,15 @@ var CircuitBreakerManager = class {
337
448
  if (!options) {
338
449
  return;
339
450
  }
451
+ this.pruneIfNeeded();
340
452
  const failureThreshold = options.failureThreshold ?? 3;
341
453
  const cooldownMs = options.cooldownMs ?? 3e4;
342
- const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
454
+ const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
343
455
  state.failures += 1;
344
456
  if (state.failures >= failureThreshold) {
345
457
  state.openUntil = Date.now() + cooldownMs;
346
458
  }
347
459
  this.breakers.set(key, state);
348
- this.pruneIfNeeded();
349
460
  }
350
461
  recordSuccess(key) {
351
462
  this.breakers.delete(key);
@@ -356,8 +467,7 @@ var CircuitBreakerManager = class {
356
467
  return false;
357
468
  }
358
469
  if (state.openUntil <= Date.now()) {
359
- state.openUntil = null;
360
- state.failures = 0;
470
+ this.breakers.delete(key);
361
471
  return false;
362
472
  }
363
473
  return true;
@@ -381,15 +491,20 @@ var CircuitBreakerManager = class {
381
491
  if (this.breakers.size <= this.maxEntries) {
382
492
  return;
383
493
  }
494
+ const now = Date.now();
384
495
  for (const [key, state] of this.breakers.entries()) {
385
496
  if (this.breakers.size <= this.maxEntries) {
386
- break;
497
+ return;
387
498
  }
388
- if (!state.openUntil || state.openUntil <= Date.now()) {
499
+ if (!state.openUntil || state.openUntil <= now) {
389
500
  this.breakers.delete(key);
390
501
  }
391
502
  }
392
- for (const key of this.breakers.keys()) {
503
+ if (this.breakers.size <= this.maxEntries) {
504
+ return;
505
+ }
506
+ const sorted = [...this.breakers.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
507
+ for (const [key] of sorted) {
393
508
  if (this.breakers.size <= this.maxEntries) {
394
509
  break;
395
510
  }
@@ -399,6 +514,7 @@ var CircuitBreakerManager = class {
399
514
  };
400
515
 
401
516
  // src/internal/FetchRateLimiter.ts
517
+ var MAX_BUCKETS = 1e4;
402
518
  var FetchRateLimiter = class {
403
519
  buckets = /* @__PURE__ */ new Map();
404
520
  queuesByBucket = /* @__PURE__ */ new Map();
@@ -564,10 +680,25 @@ var FetchRateLimiter = class {
564
680
  if (existing) {
565
681
  return existing;
566
682
  }
683
+ if (this.buckets.size >= MAX_BUCKETS) {
684
+ this.evictIdleBuckets();
685
+ }
567
686
  const bucket = { active: 0, startedAt: [] };
568
687
  this.buckets.set(bucketKey, bucket);
569
688
  return bucket;
570
689
  }
690
+ evictIdleBuckets() {
691
+ for (const [key, bucket] of this.buckets.entries()) {
692
+ if (this.buckets.size <= MAX_BUCKETS * 0.9) {
693
+ break;
694
+ }
695
+ if (bucket.active === 0 && bucket.startedAt.length === 0 && !this.queuesByBucket.has(key)) {
696
+ this.buckets.delete(key);
697
+ this.queuesByBucket.delete(key);
698
+ this.pendingBuckets.delete(key);
699
+ }
700
+ }
701
+ }
571
702
  cleanupBucket(bucketKey, bucket, intervalMs) {
572
703
  const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
573
704
  if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
@@ -894,66 +1025,18 @@ var TtlResolver = class {
894
1025
  return;
895
1026
  }
896
1027
  const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
897
- let removed = 0;
898
- for (const key of this.accessProfiles.keys()) {
899
- if (removed >= toRemove) {
900
- break;
901
- }
902
- this.accessProfiles.delete(key);
903
- removed += 1;
904
- }
905
- }
906
- };
907
-
908
- // src/invalidation/PatternMatcher.ts
909
- var PatternMatcher = class _PatternMatcher {
910
- /**
911
- * Tests whether a glob-style pattern matches a value.
912
- * Supports `*` (any sequence of characters) and `?` (any single character).
913
- * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
914
- * quadratic memory usage on long patterns/keys.
915
- */
916
- static matches(pattern, value) {
917
- return _PatternMatcher.matchLinear(pattern, value);
918
- }
919
- /**
920
- * Linear-time glob matching with O(1) extra memory.
921
- */
922
- static matchLinear(pattern, value) {
923
- let patternIndex = 0;
924
- let valueIndex = 0;
925
- let starIndex = -1;
926
- let backtrackValueIndex = 0;
927
- while (valueIndex < value.length) {
928
- const patternChar = pattern[patternIndex];
929
- const valueChar = value[valueIndex];
930
- if (patternChar === "*" && patternIndex < pattern.length) {
931
- starIndex = patternIndex;
932
- patternIndex += 1;
933
- backtrackValueIndex = valueIndex;
934
- continue;
935
- }
936
- if (patternChar === "?" || patternChar === valueChar) {
937
- patternIndex += 1;
938
- valueIndex += 1;
939
- continue;
940
- }
941
- if (starIndex !== -1) {
942
- patternIndex = starIndex + 1;
943
- backtrackValueIndex += 1;
944
- valueIndex = backtrackValueIndex;
945
- continue;
1028
+ const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
1029
+ for (let i = 0; i < toRemove && i < sorted.length; i++) {
1030
+ const entry = sorted[i];
1031
+ if (entry) {
1032
+ this.accessProfiles.delete(entry[0]);
946
1033
  }
947
- return false;
948
- }
949
- while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
950
- patternIndex += 1;
951
1034
  }
952
- return patternIndex === pattern.length;
953
1035
  }
954
1036
  };
955
1037
 
956
1038
  // src/invalidation/TagIndex.ts
1039
+ var MAX_PATTERN_RECURSION_DEPTH = 500;
957
1040
  var TagIndex = class {
958
1041
  tagToKeys = /* @__PURE__ */ new Map();
959
1042
  keyToTags = /* @__PURE__ */ new Map();
@@ -1008,7 +1091,7 @@ var TagIndex = class {
1008
1091
  }
1009
1092
  async matchPattern(pattern) {
1010
1093
  const matches = /* @__PURE__ */ new Set();
1011
- this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
1094
+ this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
1012
1095
  return [...matches];
1013
1096
  }
1014
1097
  async clear() {
@@ -1060,7 +1143,10 @@ var TagIndex = class {
1060
1143
  this.collectFromNode(child, `${prefix}${character}`, matches);
1061
1144
  }
1062
1145
  }
1063
- collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
1146
+ collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
1147
+ if (depth > MAX_PATTERN_RECURSION_DEPTH) {
1148
+ return;
1149
+ }
1064
1150
  const stateKey = `${node.id}:${patternIndex}`;
1065
1151
  if (visited.has(stateKey)) {
1066
1152
  return;
@@ -1077,21 +1163,37 @@ var TagIndex = class {
1077
1163
  return;
1078
1164
  }
1079
1165
  if (patternChar === "*") {
1080
- this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
1166
+ this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
1081
1167
  for (const [character, child2] of node.children) {
1082
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
1168
+ this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
1083
1169
  }
1084
1170
  return;
1085
1171
  }
1086
1172
  if (patternChar === "?") {
1087
1173
  for (const [character, child2] of node.children) {
1088
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex + 1, matches, visited);
1174
+ this.collectPatternMatches(
1175
+ child2,
1176
+ `${prefix}${character}`,
1177
+ pattern,
1178
+ patternIndex + 1,
1179
+ matches,
1180
+ visited,
1181
+ depth + 1
1182
+ );
1089
1183
  }
1090
1184
  return;
1091
1185
  }
1092
1186
  const child = node.children.get(patternChar);
1093
1187
  if (child) {
1094
- this.collectPatternMatches(child, `${prefix}${patternChar}`, pattern, patternIndex + 1, matches, visited);
1188
+ this.collectPatternMatches(
1189
+ child,
1190
+ `${prefix}${patternChar}`,
1191
+ pattern,
1192
+ patternIndex + 1,
1193
+ matches,
1194
+ visited,
1195
+ depth + 1
1196
+ );
1095
1197
  }
1096
1198
  }
1097
1199
  pruneKnownKeysIfNeeded() {
@@ -1234,6 +1336,7 @@ var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
1234
1336
  var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1235
1337
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1236
1338
  var MAX_CACHE_KEY_LENGTH = 1024;
1339
+ var MAX_PATTERN_LENGTH = 1024;
1237
1340
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
1238
1341
  var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1239
1342
  var DebugLogger = class {
@@ -1282,6 +1385,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
1282
1385
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
1283
1386
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
1284
1387
  this.tagIndex = options.tagIndex ?? new TagIndex();
1388
+ this.keyDiscovery = new CacheKeyDiscovery({
1389
+ layers: this.layers,
1390
+ tagIndex: this.tagIndex,
1391
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
1392
+ handleLayerFailure: async (layer, operation, error) => {
1393
+ await this.handleLayerFailure(layer, operation, error);
1394
+ }
1395
+ });
1285
1396
  if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
1286
1397
  this.logger.warn?.(
1287
1398
  "Using the default in-memory TagIndex with a shared cache layer only tracks keys seen by this process. Use RedisTagIndex for cross-instance tag invalidation."
@@ -1309,6 +1420,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1309
1420
  unsubscribeInvalidation;
1310
1421
  logger;
1311
1422
  tagIndex;
1423
+ keyDiscovery;
1312
1424
  fetchRateLimiter = new FetchRateLimiter();
1313
1425
  snapshotSerializer = new JsonSerializer();
1314
1426
  backgroundRefreshes = /* @__PURE__ */ new Map();
@@ -1658,15 +1770,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
1658
1770
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1659
1771
  }
1660
1772
  async invalidateByPattern(pattern) {
1773
+ this.validatePattern(pattern);
1661
1774
  await this.awaitStartup("invalidateByPattern");
1662
- const keys = await this.collectKeysMatchingPattern(this.qualifyPattern(pattern));
1775
+ const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
1663
1776
  await this.deleteKeys(keys);
1664
1777
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1665
1778
  }
1666
1779
  async invalidateByPrefix(prefix) {
1667
1780
  await this.awaitStartup("invalidateByPrefix");
1668
1781
  const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1669
- const keys = await this.collectKeysWithPrefix(qualifiedPrefix);
1782
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
1670
1783
  await this.deleteKeys(keys);
1671
1784
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1672
1785
  }
@@ -2279,50 +2392,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
2279
2392
  shouldBroadcastL1Invalidation() {
2280
2393
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
2281
2394
  }
2282
- async collectKeysWithPrefix(prefix) {
2283
- const matches = new Set(
2284
- this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(prefix) : await this.tagIndex.matchPattern(`${prefix}*`)
2285
- );
2286
- await Promise.all(
2287
- this.layers.map(async (layer) => {
2288
- if (!layer.keys || this.shouldSkipLayer(layer)) {
2289
- return;
2290
- }
2291
- try {
2292
- const keys = await layer.keys();
2293
- for (const key of keys) {
2294
- if (key.startsWith(prefix)) {
2295
- matches.add(key);
2296
- }
2297
- }
2298
- } catch (error) {
2299
- await this.handleLayerFailure(layer, "invalidate-prefix-scan", error);
2300
- }
2301
- })
2302
- );
2303
- return [...matches];
2304
- }
2305
- async collectKeysMatchingPattern(pattern) {
2306
- const matches = new Set(await this.tagIndex.matchPattern(pattern));
2307
- await Promise.all(
2308
- this.layers.map(async (layer) => {
2309
- if (!layer.keys || this.shouldSkipLayer(layer)) {
2310
- return;
2311
- }
2312
- try {
2313
- const keys = await layer.keys();
2314
- for (const key of keys) {
2315
- if (PatternMatcher.matches(pattern, key)) {
2316
- matches.add(key);
2317
- }
2318
- }
2319
- } catch (error) {
2320
- await this.handleLayerFailure(layer, "invalidate-pattern-scan", error);
2321
- }
2322
- })
2323
- );
2324
- return [...matches];
2325
- }
2326
2395
  shouldCleanupGenerations() {
2327
2396
  return Boolean(this.options.generationCleanup);
2328
2397
  }
@@ -2345,7 +2414,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2345
2414
  }
2346
2415
  async cleanupGeneration(generation) {
2347
2416
  const prefix = `v${generation}:`;
2348
- const keys = await this.collectKeysWithPrefix(prefix);
2417
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
2349
2418
  if (keys.length === 0) {
2350
2419
  return;
2351
2420
  }
@@ -2595,6 +2664,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
2595
2664
  }
2596
2665
  return key;
2597
2666
  }
2667
+ validatePattern(pattern) {
2668
+ if (pattern.length === 0) {
2669
+ throw new Error("Pattern must not be empty.");
2670
+ }
2671
+ if (pattern.length > MAX_PATTERN_LENGTH) {
2672
+ throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
2673
+ }
2674
+ if (/[\u0000-\u001F\u007F]/.test(pattern)) {
2675
+ throw new Error("Pattern contains unsupported control characters.");
2676
+ }
2677
+ }
2598
2678
  validateTtlPolicy(name, policy) {
2599
2679
  if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2600
2680
  return;
@@ -2759,7 +2839,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
2759
2839
  }
2760
2840
  };
2761
2841
  function createInstanceId() {
2762
- return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2842
+ if (globalThis.crypto?.randomUUID) {
2843
+ return globalThis.crypto.randomUUID();
2844
+ }
2845
+ const bytes = new Uint8Array(16);
2846
+ if (globalThis.crypto?.getRandomValues) {
2847
+ globalThis.crypto.getRandomValues(bytes);
2848
+ } else {
2849
+ for (let i = 0; i < bytes.length; i++) {
2850
+ bytes[i] = Math.floor(Math.random() * 256);
2851
+ }
2852
+ }
2853
+ return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
2763
2854
  }
2764
2855
 
2765
2856
  // src/invalidation/RedisInvalidationBus.ts
@@ -2801,7 +2892,7 @@ var RedisInvalidationBus = class {
2801
2892
  async dispatchToHandlers(payload) {
2802
2893
  let message;
2803
2894
  try {
2804
- const parsed = JSON.parse(payload);
2895
+ const parsed = sanitizeJsonValue2(JSON.parse(payload));
2805
2896
  if (!this.isInvalidationMessage(parsed)) {
2806
2897
  throw new Error("Invalid invalidation payload shape.");
2807
2898
  }
@@ -2838,6 +2929,22 @@ var RedisInvalidationBus = class {
2838
2929
  console.error(`[layercache] ${message}`, error);
2839
2930
  }
2840
2931
  };
2932
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2933
+ function sanitizeJsonValue2(value) {
2934
+ if (Array.isArray(value)) {
2935
+ return value.map(sanitizeJsonValue2);
2936
+ }
2937
+ if (value && typeof value === "object") {
2938
+ const result = /* @__PURE__ */ Object.create(null);
2939
+ for (const key of Object.keys(value)) {
2940
+ if (!DANGEROUS_KEYS.has(key)) {
2941
+ result[key] = sanitizeJsonValue2(value[key]);
2942
+ }
2943
+ }
2944
+ return result;
2945
+ }
2946
+ return value;
2947
+ }
2841
2948
 
2842
2949
  // src/invalidation/RedisTagIndex.ts
2843
2950
  var RedisTagIndex = class {
@@ -2978,6 +3085,8 @@ function createCacheStatsHandler(cache) {
2978
3085
  return async (_request, response) => {
2979
3086
  response.statusCode = 200;
2980
3087
  response.setHeader?.("content-type", "application/json; charset=utf-8");
3088
+ response.setHeader?.("cache-control", "no-store");
3089
+ response.setHeader?.("x-content-type-options", "nosniff");
2981
3090
  response.end(JSON.stringify(cache.getStats(), null, 2));
2982
3091
  };
2983
3092
  }
@@ -3063,7 +3172,7 @@ function normalizeUrl(url) {
3063
3172
  try {
3064
3173
  const parsed = new URL(url, "http://localhost");
3065
3174
  parsed.searchParams.sort();
3066
- return decodeURIComponent(parsed.pathname) + parsed.search;
3175
+ return parsed.pathname + parsed.search;
3067
3176
  } catch {
3068
3177
  return url;
3069
3178
  }
@@ -3114,7 +3223,7 @@ function normalizeUrl2(url) {
3114
3223
  try {
3115
3224
  const parsed = new URL(url, "http://localhost");
3116
3225
  parsed.searchParams.sort();
3117
- return decodeURIComponent(parsed.pathname) + parsed.search;
3226
+ return parsed.pathname + parsed.search;
3118
3227
  } catch {
3119
3228
  return url;
3120
3229
  }
@@ -3577,8 +3686,9 @@ var RedisLayer = class {
3577
3686
  }
3578
3687
  }
3579
3688
  try {
3580
- await this.client.del(this.withPrefix(key)).catch(() => void 0);
3581
- } catch {
3689
+ await this.client.del(this.withPrefix(key));
3690
+ } catch (deleteError) {
3691
+ console.warn(`[layercache] RedisLayer: failed to delete corrupted key "${key}"`, deleteError);
3582
3692
  }
3583
3693
  return null;
3584
3694
  }
@@ -3978,7 +4088,7 @@ var MemcachedLayer = class {
3978
4088
 
3979
4089
  // src/serialization/MsgpackSerializer.ts
3980
4090
  var import_msgpack = require("@msgpack/msgpack");
3981
- var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
4091
+ var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3982
4092
  var MsgpackSerializer = class {
3983
4093
  serialize(value) {
3984
4094
  return Buffer.from((0, import_msgpack.encode)(value));
@@ -3997,7 +4107,7 @@ function sanitizeMsgpackValue(value) {
3997
4107
  }
3998
4108
  const sanitized = {};
3999
4109
  for (const [key, entry] of Object.entries(value)) {
4000
- if (DANGEROUS_KEYS.has(key)) {
4110
+ if (DANGEROUS_KEYS2.has(key)) {
4001
4111
  continue;
4002
4112
  }
4003
4113
  sanitized[key] = sanitizeMsgpackValue(entry);
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-B_rUqDy6.cjs';
2
- export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-B_rUqDy6.cjs';
1
+ import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-P07GCO2Y.cjs';
2
+ export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-P07GCO2Y.cjs';
3
3
  import Redis from 'ioredis';
4
4
  import 'node:events';
5
5
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-B_rUqDy6.js';
2
- export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-B_rUqDy6.js';
1
+ import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-P07GCO2Y.js';
2
+ export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-P07GCO2Y.js';
3
3
  import Redis from 'ioredis';
4
4
  import 'node:events';
5
5