layercache 1.2.4 → 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/README.md CHANGED
@@ -203,6 +203,8 @@ Glob-style deletion against the tracked key set, plus any layer that can enumera
203
203
  await cache.invalidateByPattern('user:*') // deletes user:1, user:2, …
204
204
  ```
205
205
 
206
+ Patterns must be non-empty, at most 1024 characters long, and free of control characters.
207
+
206
208
  For multi-instance deployments, prefer a shared `RedisTagIndex`. Without it, pattern invalidation still scans real layer keys when available, but that fallback only helps on layers that implement `keys()`, and tag tracking itself remains process-local.
207
209
 
208
210
  ### `cache.invalidateByPrefix(prefix): Promise<void>`
@@ -320,6 +322,8 @@ await cache.warm(
320
322
 
321
323
  Returns a scoped view with the same full API (`get`, `set`, `delete`, `clear`, `mget`, `wrap`, `warm`, `invalidateByTag`, `invalidateByPattern`, `getMetrics`). `clear()` only touches `prefix:*` keys, and namespace metrics are serialized per `CacheStack` instance so unrelated caches do not block each other while metrics are collected.
322
324
 
325
+ Namespace prefixes must be non-empty, at most 256 characters long, and free of control characters.
326
+
323
327
  ```ts
324
328
  const users = cache.namespace('users')
325
329
  const posts = cache.namespace('posts')
@@ -712,6 +716,8 @@ http.createServer(statsHandler).listen(9090)
712
716
  // GET / → JSON stats
713
717
  ```
714
718
 
719
+ The built-in handler returns JSON with `Cache-Control: no-store` and `X-Content-Type-Options: nosniff` headers.
720
+
715
721
  Or use the Fastify plugin:
716
722
 
717
723
  ```ts
@@ -185,6 +185,7 @@ var MemoryLayer = class {
185
185
  };
186
186
 
187
187
  // src/invalidation/TagIndex.ts
188
+ var MAX_PATTERN_RECURSION_DEPTH = 500;
188
189
  var TagIndex = class {
189
190
  tagToKeys = /* @__PURE__ */ new Map();
190
191
  keyToTags = /* @__PURE__ */ new Map();
@@ -239,7 +240,7 @@ var TagIndex = class {
239
240
  }
240
241
  async matchPattern(pattern) {
241
242
  const matches = /* @__PURE__ */ new Set();
242
- this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
243
+ this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
243
244
  return [...matches];
244
245
  }
245
246
  async clear() {
@@ -291,7 +292,10 @@ var TagIndex = class {
291
292
  this.collectFromNode(child, `${prefix}${character}`, matches);
292
293
  }
293
294
  }
294
- collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
295
+ collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
296
+ if (depth > MAX_PATTERN_RECURSION_DEPTH) {
297
+ return;
298
+ }
295
299
  const stateKey = `${node.id}:${patternIndex}`;
296
300
  if (visited.has(stateKey)) {
297
301
  return;
@@ -308,21 +312,37 @@ var TagIndex = class {
308
312
  return;
309
313
  }
310
314
  if (patternChar === "*") {
311
- this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
315
+ this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
312
316
  for (const [character, child2] of node.children) {
313
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
317
+ this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
314
318
  }
315
319
  return;
316
320
  }
317
321
  if (patternChar === "?") {
318
322
  for (const [character, child2] of node.children) {
319
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex + 1, matches, visited);
323
+ this.collectPatternMatches(
324
+ child2,
325
+ `${prefix}${character}`,
326
+ pattern,
327
+ patternIndex + 1,
328
+ matches,
329
+ visited,
330
+ depth + 1
331
+ );
320
332
  }
321
333
  return;
322
334
  }
323
335
  const child = node.children.get(patternChar);
324
336
  if (child) {
325
- this.collectPatternMatches(child, `${prefix}${patternChar}`, pattern, patternIndex + 1, matches, visited);
337
+ this.collectPatternMatches(
338
+ child,
339
+ `${prefix}${patternChar}`,
340
+ pattern,
341
+ patternIndex + 1,
342
+ matches,
343
+ visited,
344
+ depth + 1
345
+ );
326
346
  }
327
347
  }
328
348
  pruneKnownKeysIfNeeded() {
@@ -423,7 +443,7 @@ function normalizeUrl(url) {
423
443
  try {
424
444
  const parsed = new URL(url, "http://localhost");
425
445
  parsed.searchParams.sort();
426
- return decodeURIComponent(parsed.pathname) + parsed.search;
446
+ return parsed.pathname + parsed.search;
427
447
  } catch {
428
448
  return url;
429
449
  }
package/dist/cli.cjs CHANGED
@@ -281,10 +281,7 @@ async function main(argv = process.argv.slice(2)) {
281
281
  }
282
282
  const redisUrl = validateRedisUrl(args.redisUrl);
283
283
  if (!redisUrl) {
284
- process.stderr.write(
285
- `Error: invalid Redis URL "${maskRedisUrl(args.redisUrl)}". Expected format: redis://[user:password@]host[:port][/db]
286
- `
287
- );
284
+ process.stderr.write("Error: invalid Redis URL. Expected format: redis://[user:password@]host[:port][/db]\n");
288
285
  process.exitCode = 1;
289
286
  return;
290
287
  }
@@ -296,7 +293,7 @@ async function main(argv = process.argv.slice(2)) {
296
293
  try {
297
294
  await redis.connect().catch((error) => {
298
295
  const message = error instanceof Error ? error.message : String(error);
299
- throw new Error(`Failed to connect to Redis at "${maskRedisUrl(redisUrl)}": ${message}`);
296
+ throw new Error(`Failed to connect to Redis: ${message}`);
300
297
  });
301
298
  if (args.command === "stats") {
302
299
  const keys = await scanKeys(redis, args.pattern ?? "*");
@@ -451,17 +448,6 @@ function summarizeInspectableValue(value) {
451
448
  }
452
449
  return value;
453
450
  }
454
- function maskRedisUrl(url) {
455
- try {
456
- const parsed = new URL(url);
457
- if (parsed.password) {
458
- parsed.password = "***";
459
- }
460
- return parsed.toString();
461
- } catch {
462
- return url.replace(/:([^@/]+)@/, ":***@");
463
- }
464
- }
465
451
  if (process.argv[1]?.includes("cli.")) {
466
452
  void main();
467
453
  }
package/dist/cli.js CHANGED
@@ -19,10 +19,7 @@ async function main(argv = process.argv.slice(2)) {
19
19
  }
20
20
  const redisUrl = validateRedisUrl(args.redisUrl);
21
21
  if (!redisUrl) {
22
- process.stderr.write(
23
- `Error: invalid Redis URL "${maskRedisUrl(args.redisUrl)}". Expected format: redis://[user:password@]host[:port][/db]
24
- `
25
- );
22
+ process.stderr.write("Error: invalid Redis URL. Expected format: redis://[user:password@]host[:port][/db]\n");
26
23
  process.exitCode = 1;
27
24
  return;
28
25
  }
@@ -34,7 +31,7 @@ async function main(argv = process.argv.slice(2)) {
34
31
  try {
35
32
  await redis.connect().catch((error) => {
36
33
  const message = error instanceof Error ? error.message : String(error);
37
- throw new Error(`Failed to connect to Redis at "${maskRedisUrl(redisUrl)}": ${message}`);
34
+ throw new Error(`Failed to connect to Redis: ${message}`);
38
35
  });
39
36
  if (args.command === "stats") {
40
37
  const keys = await scanKeys(redis, args.pattern ?? "*");
@@ -189,17 +186,6 @@ function summarizeInspectableValue(value) {
189
186
  }
190
187
  return value;
191
188
  }
192
- function maskRedisUrl(url) {
193
- try {
194
- const parsed = new URL(url);
195
- if (parsed.password) {
196
- parsed.password = "***";
197
- }
198
- return parsed.toString();
199
- } catch {
200
- return url.replace(/:([^@/]+)@/, ":***@");
201
- }
202
- }
203
189
  if (process.argv[1]?.includes("cli.")) {
204
190
  void main();
205
191
  }
@@ -663,6 +663,7 @@ declare class CacheStack extends EventEmitter {
663
663
  private validateRateLimitOptions;
664
664
  private validateNonNegativeNumber;
665
665
  private validateCacheKey;
666
+ private validatePattern;
666
667
  private validateTtlPolicy;
667
668
  private assertActive;
668
669
  private awaitStartup;
@@ -663,6 +663,7 @@ declare class CacheStack extends EventEmitter {
663
663
  private validateRateLimitOptions;
664
664
  private validateNonNegativeNumber;
665
665
  private validateCacheKey;
666
+ private validatePattern;
666
667
  private validateTtlPolicy;
667
668
  private assertActive;
668
669
  private awaitStartup;
package/dist/edge.cjs CHANGED
@@ -295,6 +295,7 @@ var PatternMatcher = class _PatternMatcher {
295
295
  };
296
296
 
297
297
  // src/invalidation/TagIndex.ts
298
+ var MAX_PATTERN_RECURSION_DEPTH = 500;
298
299
  var TagIndex = class {
299
300
  tagToKeys = /* @__PURE__ */ new Map();
300
301
  keyToTags = /* @__PURE__ */ new Map();
@@ -349,7 +350,7 @@ var TagIndex = class {
349
350
  }
350
351
  async matchPattern(pattern) {
351
352
  const matches = /* @__PURE__ */ new Set();
352
- this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
353
+ this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
353
354
  return [...matches];
354
355
  }
355
356
  async clear() {
@@ -401,7 +402,10 @@ var TagIndex = class {
401
402
  this.collectFromNode(child, `${prefix}${character}`, matches);
402
403
  }
403
404
  }
404
- collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
405
+ collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
406
+ if (depth > MAX_PATTERN_RECURSION_DEPTH) {
407
+ return;
408
+ }
405
409
  const stateKey = `${node.id}:${patternIndex}`;
406
410
  if (visited.has(stateKey)) {
407
411
  return;
@@ -418,21 +422,37 @@ var TagIndex = class {
418
422
  return;
419
423
  }
420
424
  if (patternChar === "*") {
421
- this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
425
+ this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
422
426
  for (const [character, child2] of node.children) {
423
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
427
+ this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
424
428
  }
425
429
  return;
426
430
  }
427
431
  if (patternChar === "?") {
428
432
  for (const [character, child2] of node.children) {
429
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex + 1, matches, visited);
433
+ this.collectPatternMatches(
434
+ child2,
435
+ `${prefix}${character}`,
436
+ pattern,
437
+ patternIndex + 1,
438
+ matches,
439
+ visited,
440
+ depth + 1
441
+ );
430
442
  }
431
443
  return;
432
444
  }
433
445
  const child = node.children.get(patternChar);
434
446
  if (child) {
435
- this.collectPatternMatches(child, `${prefix}${patternChar}`, pattern, patternIndex + 1, matches, visited);
447
+ this.collectPatternMatches(
448
+ child,
449
+ `${prefix}${patternChar}`,
450
+ pattern,
451
+ patternIndex + 1,
452
+ matches,
453
+ visited,
454
+ depth + 1
455
+ );
436
456
  }
437
457
  }
438
458
  pruneKnownKeysIfNeeded() {
@@ -533,7 +553,7 @@ function normalizeUrl(url) {
533
553
  try {
534
554
  const parsed = new URL(url, "http://localhost");
535
555
  parsed.searchParams.sort();
536
- return decodeURIComponent(parsed.pathname) + parsed.search;
556
+ return parsed.pathname + parsed.search;
537
557
  } catch {
538
558
  return url;
539
559
  }
package/dist/edge.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-Dw97n89L.cjs';
1
+ export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-P07GCO2Y.cjs';
2
2
  import 'node:events';
package/dist/edge.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-Dw97n89L.js';
1
+ export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-P07GCO2Y.js';
2
2
  import 'node:events';
package/dist/edge.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  MemoryLayer,
3
3
  TagIndex,
4
4
  createHonoCacheMiddleware
5
- } from "./chunk-KOYGHLVP.js";
5
+ } from "./chunk-JC26W3KK.js";
6
6
  import {
7
7
  PatternMatcher
8
8
  } from "./chunk-7V7XAB74.js";
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,17 @@ 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
+ }
308
320
 
309
321
  // src/invalidation/PatternMatcher.ts
310
322
  var PatternMatcher = class _PatternMatcher {
@@ -425,9 +437,7 @@ var CircuitBreakerManager = class {
425
437
  }
426
438
  const now = Date.now();
427
439
  if (state.openUntil <= now) {
428
- state.openUntil = null;
429
- state.failures = 0;
430
- this.breakers.set(key, state);
440
+ this.breakers.delete(key);
431
441
  return;
432
442
  }
433
443
  const remainingMs = state.openUntil - now;
@@ -438,15 +448,15 @@ var CircuitBreakerManager = class {
438
448
  if (!options) {
439
449
  return;
440
450
  }
451
+ this.pruneIfNeeded();
441
452
  const failureThreshold = options.failureThreshold ?? 3;
442
453
  const cooldownMs = options.cooldownMs ?? 3e4;
443
- const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
454
+ const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
444
455
  state.failures += 1;
445
456
  if (state.failures >= failureThreshold) {
446
457
  state.openUntil = Date.now() + cooldownMs;
447
458
  }
448
459
  this.breakers.set(key, state);
449
- this.pruneIfNeeded();
450
460
  }
451
461
  recordSuccess(key) {
452
462
  this.breakers.delete(key);
@@ -457,8 +467,7 @@ var CircuitBreakerManager = class {
457
467
  return false;
458
468
  }
459
469
  if (state.openUntil <= Date.now()) {
460
- state.openUntil = null;
461
- state.failures = 0;
470
+ this.breakers.delete(key);
462
471
  return false;
463
472
  }
464
473
  return true;
@@ -482,15 +491,20 @@ var CircuitBreakerManager = class {
482
491
  if (this.breakers.size <= this.maxEntries) {
483
492
  return;
484
493
  }
494
+ const now = Date.now();
485
495
  for (const [key, state] of this.breakers.entries()) {
486
496
  if (this.breakers.size <= this.maxEntries) {
487
- break;
497
+ return;
488
498
  }
489
- if (!state.openUntil || state.openUntil <= Date.now()) {
499
+ if (!state.openUntil || state.openUntil <= now) {
490
500
  this.breakers.delete(key);
491
501
  }
492
502
  }
493
- 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) {
494
508
  if (this.breakers.size <= this.maxEntries) {
495
509
  break;
496
510
  }
@@ -500,6 +514,7 @@ var CircuitBreakerManager = class {
500
514
  };
501
515
 
502
516
  // src/internal/FetchRateLimiter.ts
517
+ var MAX_BUCKETS = 1e4;
503
518
  var FetchRateLimiter = class {
504
519
  buckets = /* @__PURE__ */ new Map();
505
520
  queuesByBucket = /* @__PURE__ */ new Map();
@@ -665,10 +680,25 @@ var FetchRateLimiter = class {
665
680
  if (existing) {
666
681
  return existing;
667
682
  }
683
+ if (this.buckets.size >= MAX_BUCKETS) {
684
+ this.evictIdleBuckets();
685
+ }
668
686
  const bucket = { active: 0, startedAt: [] };
669
687
  this.buckets.set(bucketKey, bucket);
670
688
  return bucket;
671
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
+ }
672
702
  cleanupBucket(bucketKey, bucket, intervalMs) {
673
703
  const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
674
704
  if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
@@ -995,18 +1025,18 @@ var TtlResolver = class {
995
1025
  return;
996
1026
  }
997
1027
  const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
998
- let removed = 0;
999
- for (const key of this.accessProfiles.keys()) {
1000
- if (removed >= toRemove) {
1001
- break;
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]);
1002
1033
  }
1003
- this.accessProfiles.delete(key);
1004
- removed += 1;
1005
1034
  }
1006
1035
  }
1007
1036
  };
1008
1037
 
1009
1038
  // src/invalidation/TagIndex.ts
1039
+ var MAX_PATTERN_RECURSION_DEPTH = 500;
1010
1040
  var TagIndex = class {
1011
1041
  tagToKeys = /* @__PURE__ */ new Map();
1012
1042
  keyToTags = /* @__PURE__ */ new Map();
@@ -1061,7 +1091,7 @@ var TagIndex = class {
1061
1091
  }
1062
1092
  async matchPattern(pattern) {
1063
1093
  const matches = /* @__PURE__ */ new Set();
1064
- this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
1094
+ this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
1065
1095
  return [...matches];
1066
1096
  }
1067
1097
  async clear() {
@@ -1113,7 +1143,10 @@ var TagIndex = class {
1113
1143
  this.collectFromNode(child, `${prefix}${character}`, matches);
1114
1144
  }
1115
1145
  }
1116
- 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
+ }
1117
1150
  const stateKey = `${node.id}:${patternIndex}`;
1118
1151
  if (visited.has(stateKey)) {
1119
1152
  return;
@@ -1130,21 +1163,37 @@ var TagIndex = class {
1130
1163
  return;
1131
1164
  }
1132
1165
  if (patternChar === "*") {
1133
- this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
1166
+ this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
1134
1167
  for (const [character, child2] of node.children) {
1135
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
1168
+ this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
1136
1169
  }
1137
1170
  return;
1138
1171
  }
1139
1172
  if (patternChar === "?") {
1140
1173
  for (const [character, child2] of node.children) {
1141
- 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
+ );
1142
1183
  }
1143
1184
  return;
1144
1185
  }
1145
1186
  const child = node.children.get(patternChar);
1146
1187
  if (child) {
1147
- 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
+ );
1148
1197
  }
1149
1198
  }
1150
1199
  pruneKnownKeysIfNeeded() {
@@ -1287,6 +1336,7 @@ var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
1287
1336
  var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1288
1337
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1289
1338
  var MAX_CACHE_KEY_LENGTH = 1024;
1339
+ var MAX_PATTERN_LENGTH = 1024;
1290
1340
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
1291
1341
  var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1292
1342
  var DebugLogger = class {
@@ -1720,6 +1770,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1720
1770
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1721
1771
  }
1722
1772
  async invalidateByPattern(pattern) {
1773
+ this.validatePattern(pattern);
1723
1774
  await this.awaitStartup("invalidateByPattern");
1724
1775
  const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
1725
1776
  await this.deleteKeys(keys);
@@ -2613,6 +2664,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
2613
2664
  }
2614
2665
  return key;
2615
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
+ }
2616
2678
  validateTtlPolicy(name, policy) {
2617
2679
  if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2618
2680
  return;
@@ -2777,7 +2839,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
2777
2839
  }
2778
2840
  };
2779
2841
  function createInstanceId() {
2780
- 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("")}`;
2781
2854
  }
2782
2855
 
2783
2856
  // src/invalidation/RedisInvalidationBus.ts
@@ -2819,7 +2892,7 @@ var RedisInvalidationBus = class {
2819
2892
  async dispatchToHandlers(payload) {
2820
2893
  let message;
2821
2894
  try {
2822
- const parsed = JSON.parse(payload);
2895
+ const parsed = sanitizeJsonValue2(JSON.parse(payload));
2823
2896
  if (!this.isInvalidationMessage(parsed)) {
2824
2897
  throw new Error("Invalid invalidation payload shape.");
2825
2898
  }
@@ -2856,6 +2929,22 @@ var RedisInvalidationBus = class {
2856
2929
  console.error(`[layercache] ${message}`, error);
2857
2930
  }
2858
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
+ }
2859
2948
 
2860
2949
  // src/invalidation/RedisTagIndex.ts
2861
2950
  var RedisTagIndex = class {
@@ -2996,6 +3085,8 @@ function createCacheStatsHandler(cache) {
2996
3085
  return async (_request, response) => {
2997
3086
  response.statusCode = 200;
2998
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");
2999
3090
  response.end(JSON.stringify(cache.getStats(), null, 2));
3000
3091
  };
3001
3092
  }
@@ -3081,7 +3172,7 @@ function normalizeUrl(url) {
3081
3172
  try {
3082
3173
  const parsed = new URL(url, "http://localhost");
3083
3174
  parsed.searchParams.sort();
3084
- return decodeURIComponent(parsed.pathname) + parsed.search;
3175
+ return parsed.pathname + parsed.search;
3085
3176
  } catch {
3086
3177
  return url;
3087
3178
  }
@@ -3132,7 +3223,7 @@ function normalizeUrl2(url) {
3132
3223
  try {
3133
3224
  const parsed = new URL(url, "http://localhost");
3134
3225
  parsed.searchParams.sort();
3135
- return decodeURIComponent(parsed.pathname) + parsed.search;
3226
+ return parsed.pathname + parsed.search;
3136
3227
  } catch {
3137
3228
  return url;
3138
3229
  }
@@ -3595,8 +3686,9 @@ var RedisLayer = class {
3595
3686
  }
3596
3687
  }
3597
3688
  try {
3598
- await this.client.del(this.withPrefix(key)).catch(() => void 0);
3599
- } catch {
3689
+ await this.client.del(this.withPrefix(key));
3690
+ } catch (deleteError) {
3691
+ console.warn(`[layercache] RedisLayer: failed to delete corrupted key "${key}"`, deleteError);
3600
3692
  }
3601
3693
  return null;
3602
3694
  }
@@ -3996,7 +4088,7 @@ var MemcachedLayer = class {
3996
4088
 
3997
4089
  // src/serialization/MsgpackSerializer.ts
3998
4090
  var import_msgpack = require("@msgpack/msgpack");
3999
- var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
4091
+ var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
4000
4092
  var MsgpackSerializer = class {
4001
4093
  serialize(value) {
4002
4094
  return Buffer.from((0, import_msgpack.encode)(value));
@@ -4015,7 +4107,7 @@ function sanitizeMsgpackValue(value) {
4015
4107
  }
4016
4108
  const sanitized = {};
4017
4109
  for (const [key, entry] of Object.entries(value)) {
4018
- if (DANGEROUS_KEYS.has(key)) {
4110
+ if (DANGEROUS_KEYS2.has(key)) {
4019
4111
  continue;
4020
4112
  }
4021
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-Dw97n89L.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-Dw97n89L.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-Dw97n89L.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-Dw97n89L.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