layercache 1.3.2 → 1.3.3
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 +156 -80
- package/dist/{chunk-GJBKCFE6.js → chunk-5RCAX2BQ.js} +9 -9
- package/dist/{chunk-BQLL6IM5.js → chunk-BORDQ3LA.js} +135 -0
- package/dist/cli.cjs +77 -5
- package/dist/cli.js +37 -7
- package/dist/edge.cjs +9 -9
- package/dist/edge.js +1 -1
- package/dist/index.cjs +96 -58
- package/dist/index.js +81 -156
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
RedisTagIndex
|
|
4
|
-
|
|
3
|
+
RedisTagIndex,
|
|
4
|
+
validateCacheKey,
|
|
5
|
+
validatePattern,
|
|
6
|
+
validateTag
|
|
7
|
+
} from "./chunk-BORDQ3LA.js";
|
|
5
8
|
import {
|
|
6
9
|
isStoredValueEnvelope,
|
|
7
10
|
resolveStoredValue
|
|
@@ -46,13 +49,17 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
46
49
|
throw new Error(`Failed to connect to Redis at ${maskRedisUrl(redisUrl)}: ${message}`);
|
|
47
50
|
});
|
|
48
51
|
if (args.command === "stats") {
|
|
49
|
-
const
|
|
50
|
-
|
|
52
|
+
const pattern = args.pattern ?? "*";
|
|
53
|
+
if (args.pattern && !validateCliInput(args.pattern, validatePattern)) return;
|
|
54
|
+
const keys = await scanKeys(redis, pattern);
|
|
55
|
+
process.stdout.write(`${JSON.stringify({ totalKeys: keys.length, pattern }, null, 2)}
|
|
51
56
|
`);
|
|
52
57
|
return;
|
|
53
58
|
}
|
|
54
59
|
if (args.command === "keys") {
|
|
55
|
-
const
|
|
60
|
+
const pattern = args.pattern ?? "*";
|
|
61
|
+
if (args.pattern && !validateCliInput(args.pattern, validatePattern)) return;
|
|
62
|
+
const keys = await scanKeys(redis, pattern);
|
|
56
63
|
if (keys.length > 0) {
|
|
57
64
|
process.stdout.write(`${keys.join("\n")}
|
|
58
65
|
`);
|
|
@@ -61,6 +68,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
61
68
|
}
|
|
62
69
|
if (args.command === "invalidate") {
|
|
63
70
|
if (args.tag) {
|
|
71
|
+
if (!validateCliInput(args.tag, validateTag)) return;
|
|
64
72
|
const tagIndex = new RedisTagIndex({ client: redis, prefix: args.tagIndexPrefix ?? "layercache:tag-index" });
|
|
65
73
|
const keys2 = await tagIndex.keysForTag(args.tag);
|
|
66
74
|
if (keys2.length > 0) {
|
|
@@ -70,11 +78,18 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
70
78
|
`);
|
|
71
79
|
return;
|
|
72
80
|
}
|
|
73
|
-
const
|
|
81
|
+
const effectivePattern = args.pattern ?? "*";
|
|
82
|
+
if (args.pattern && !validateCliInput(args.pattern, validatePattern)) return;
|
|
83
|
+
const keys = await scanKeys(redis, effectivePattern);
|
|
84
|
+
if (!args.pattern && !args.force && keys.length > 0) {
|
|
85
|
+
process.stderr.write(`Warning: this operation will invalidate ${keys.length} keys. Use --force to confirm.
|
|
86
|
+
`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
74
89
|
if (keys.length > 0) {
|
|
75
90
|
await batchDelete(redis, keys);
|
|
76
91
|
}
|
|
77
|
-
process.stdout.write(`${JSON.stringify({ deletedKeys: keys.length, pattern:
|
|
92
|
+
process.stdout.write(`${JSON.stringify({ deletedKeys: keys.length, pattern: effectivePattern }, null, 2)}
|
|
78
93
|
`);
|
|
79
94
|
return;
|
|
80
95
|
}
|
|
@@ -82,6 +97,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
82
97
|
if (!args.key) {
|
|
83
98
|
throw new Error("inspect requires --key <key>.");
|
|
84
99
|
}
|
|
100
|
+
if (!validateCliInput(args.key, validateCacheKey)) return;
|
|
85
101
|
const payload = await redis.getBuffer(args.key);
|
|
86
102
|
const ttl = await redis.ttl(args.key);
|
|
87
103
|
const decoded = decodeInspectablePayload(payload);
|
|
@@ -156,6 +172,8 @@ function parseArgs(argv) {
|
|
|
156
172
|
index += 1;
|
|
157
173
|
} else if (token === "--require-tls") {
|
|
158
174
|
parsed.requireTls = true;
|
|
175
|
+
} else if (token === "--force") {
|
|
176
|
+
parsed.force = true;
|
|
159
177
|
}
|
|
160
178
|
}
|
|
161
179
|
return parsed;
|
|
@@ -232,6 +250,18 @@ function maskRedisUrl(url) {
|
|
|
232
250
|
return url.replace(/:([^@/]+)@/, ":***@");
|
|
233
251
|
}
|
|
234
252
|
}
|
|
253
|
+
function validateCliInput(value, validator) {
|
|
254
|
+
try {
|
|
255
|
+
validator(value);
|
|
256
|
+
return true;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
259
|
+
process.stderr.write(`Error: ${message}
|
|
260
|
+
`);
|
|
261
|
+
process.exitCode = 1;
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
235
265
|
if (process.argv[1]?.endsWith("cli.cjs") || process.argv[1]?.endsWith("cli.js")) {
|
|
236
266
|
void main();
|
|
237
267
|
}
|
package/dist/edge.cjs
CHANGED
|
@@ -339,7 +339,7 @@ var MAX_PATTERN_RECURSION_DEPTH = 500;
|
|
|
339
339
|
var TagIndex = class {
|
|
340
340
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
341
341
|
keyToTags = /* @__PURE__ */ new Map();
|
|
342
|
-
knownKeys = /* @__PURE__ */ new
|
|
342
|
+
knownKeys = /* @__PURE__ */ new Map();
|
|
343
343
|
maxKnownKeys;
|
|
344
344
|
nextNodeId = 1;
|
|
345
345
|
root = this.createTrieNode();
|
|
@@ -427,10 +427,11 @@ var TagIndex = class {
|
|
|
427
427
|
};
|
|
428
428
|
}
|
|
429
429
|
insertKnownKey(key) {
|
|
430
|
-
|
|
430
|
+
const isNew = !this.knownKeys.has(key);
|
|
431
|
+
this.knownKeys.set(key, Date.now());
|
|
432
|
+
if (!isNew) {
|
|
431
433
|
return;
|
|
432
434
|
}
|
|
433
|
-
this.knownKeys.add(key);
|
|
434
435
|
let node = this.root;
|
|
435
436
|
for (const character of key) {
|
|
436
437
|
let child = node.children.get(character);
|
|
@@ -525,14 +526,13 @@ var TagIndex = class {
|
|
|
525
526
|
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
526
527
|
return;
|
|
527
528
|
}
|
|
529
|
+
const sorted = [...this.knownKeys.entries()].sort((a, b) => a[1] - b[1]);
|
|
528
530
|
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
529
|
-
let
|
|
530
|
-
|
|
531
|
-
if (
|
|
532
|
-
|
|
531
|
+
for (let i = 0; i < toRemove && i < sorted.length; i += 1) {
|
|
532
|
+
const entry = sorted[i];
|
|
533
|
+
if (entry) {
|
|
534
|
+
this.removeKey(entry[0]);
|
|
533
535
|
}
|
|
534
|
-
this.removeKey(key);
|
|
535
|
-
removed += 1;
|
|
536
536
|
}
|
|
537
537
|
}
|
|
538
538
|
removeKey(key) {
|
package/dist/edge.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -961,6 +961,7 @@ var CacheStackLayerWriter = class {
|
|
|
961
961
|
};
|
|
962
962
|
|
|
963
963
|
// src/internal/CacheStackMaintenance.ts
|
|
964
|
+
var MAX_KEY_EPOCHS = 5e4;
|
|
964
965
|
var CacheStackMaintenance = class {
|
|
965
966
|
keyEpochs = /* @__PURE__ */ new Map();
|
|
966
967
|
writeBehindQueue = [];
|
|
@@ -1004,6 +1005,7 @@ var CacheStackMaintenance = class {
|
|
|
1004
1005
|
for (const key of keys) {
|
|
1005
1006
|
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
1006
1007
|
}
|
|
1008
|
+
this.pruneKeyEpochsIfNeeded();
|
|
1007
1009
|
}
|
|
1008
1010
|
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
1009
1011
|
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
@@ -1056,6 +1058,16 @@ var CacheStackMaintenance = class {
|
|
|
1056
1058
|
async waitForGenerationCleanup() {
|
|
1057
1059
|
await this.generationCleanupPromise;
|
|
1058
1060
|
}
|
|
1061
|
+
pruneKeyEpochsIfNeeded() {
|
|
1062
|
+
if (this.keyEpochs.size <= MAX_KEY_EPOCHS) {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const sorted = [...this.keyEpochs.entries()].sort((a, b) => a[1] - b[1]);
|
|
1066
|
+
const toDelete = Math.ceil(sorted.length * 0.1);
|
|
1067
|
+
for (let i = 0; i < toDelete; i++) {
|
|
1068
|
+
this.keyEpochs.delete(sorted[i][0]);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1059
1071
|
};
|
|
1060
1072
|
|
|
1061
1073
|
// src/internal/CacheStackRuntimePolicy.ts
|
|
@@ -1095,16 +1107,16 @@ function planFreshReadPolicies({
|
|
|
1095
1107
|
}
|
|
1096
1108
|
|
|
1097
1109
|
// src/internal/CacheStackSnapshotManager.ts
|
|
1098
|
-
var import_node_crypto = require("crypto");
|
|
1099
1110
|
var import_node_fs = require("fs");
|
|
1100
|
-
var import_node_path = __toESM(require("path"), 1);
|
|
1101
1111
|
|
|
1102
1112
|
// src/internal/CacheSnapshotFile.ts
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1113
|
+
var import_node_crypto = require("crypto");
|
|
1114
|
+
var import_promises = require("fs/promises");
|
|
1115
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
|
|
1116
|
+
const relative = path.relative(realBaseDir, candidatePath);
|
|
1117
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
|
|
1106
1118
|
}
|
|
1107
|
-
async function findExistingAncestor(directory, fs3,
|
|
1119
|
+
async function findExistingAncestor(directory, fs3, path) {
|
|
1108
1120
|
let current = directory;
|
|
1109
1121
|
while (true) {
|
|
1110
1122
|
try {
|
|
@@ -1115,7 +1127,7 @@ async function findExistingAncestor(directory, fs3, path2) {
|
|
|
1115
1127
|
throw error;
|
|
1116
1128
|
}
|
|
1117
1129
|
}
|
|
1118
|
-
const parent =
|
|
1130
|
+
const parent = path.dirname(current);
|
|
1119
1131
|
if (parent === current) {
|
|
1120
1132
|
return current;
|
|
1121
1133
|
}
|
|
@@ -1130,36 +1142,36 @@ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = p
|
|
|
1130
1142
|
throw new Error("filePath must not contain null bytes.");
|
|
1131
1143
|
}
|
|
1132
1144
|
const { promises: fs3 } = await import("fs");
|
|
1133
|
-
const
|
|
1134
|
-
const resolved =
|
|
1135
|
-
const baseDir = snapshotBaseDir === false ? false :
|
|
1145
|
+
const path = await import("path");
|
|
1146
|
+
const resolved = path.resolve(filePath);
|
|
1147
|
+
const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
|
|
1136
1148
|
if (baseDir === false) {
|
|
1137
1149
|
return resolved;
|
|
1138
1150
|
}
|
|
1139
1151
|
await fs3.mkdir(baseDir, { recursive: true });
|
|
1140
1152
|
const realBaseDir = await fs3.realpath(baseDir);
|
|
1141
|
-
if (!isWithinSnapshotBase(realBaseDir, resolved,
|
|
1153
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
|
|
1142
1154
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1143
1155
|
}
|
|
1144
1156
|
if (mode === "read") {
|
|
1145
1157
|
const realTarget = await fs3.realpath(resolved);
|
|
1146
|
-
if (!isWithinSnapshotBase(realBaseDir, realTarget,
|
|
1158
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
|
|
1147
1159
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1148
1160
|
}
|
|
1149
1161
|
return realTarget;
|
|
1150
1162
|
}
|
|
1151
|
-
const parentDir =
|
|
1152
|
-
const existingAncestor = await findExistingAncestor(parentDir, fs3,
|
|
1163
|
+
const parentDir = path.dirname(resolved);
|
|
1164
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs3, path);
|
|
1153
1165
|
const realExistingAncestor = await fs3.realpath(existingAncestor);
|
|
1154
|
-
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor,
|
|
1166
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
|
|
1155
1167
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1156
1168
|
}
|
|
1157
1169
|
await fs3.mkdir(parentDir, { recursive: true });
|
|
1158
1170
|
const realParentDir = await fs3.realpath(parentDir);
|
|
1159
|
-
if (!isWithinSnapshotBase(realBaseDir, realParentDir,
|
|
1171
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
|
|
1160
1172
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1161
1173
|
}
|
|
1162
|
-
const targetPath =
|
|
1174
|
+
const targetPath = path.join(realParentDir, path.basename(resolved));
|
|
1163
1175
|
try {
|
|
1164
1176
|
const existing = await fs3.lstat(targetPath);
|
|
1165
1177
|
if (existing.isSymbolicLink()) {
|
|
@@ -1194,6 +1206,17 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
|
1194
1206
|
}
|
|
1195
1207
|
return Buffer.concat(chunks).toString("utf8");
|
|
1196
1208
|
}
|
|
1209
|
+
function atomicWriteTempPath(targetPath) {
|
|
1210
|
+
return `${targetPath}.tmp-${(0, import_node_crypto.randomBytes)(8).toString("hex")}`;
|
|
1211
|
+
}
|
|
1212
|
+
async function commitAtomicWrite(tempPath, targetPath) {
|
|
1213
|
+
try {
|
|
1214
|
+
await (0, import_promises.rename)(tempPath, targetPath);
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
await (0, import_promises.unlink)(tempPath).catch(() => void 0);
|
|
1217
|
+
throw error;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1197
1220
|
|
|
1198
1221
|
// src/internal/StructuredDataSanitizer.ts
|
|
1199
1222
|
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
@@ -1272,10 +1295,7 @@ var CacheStackSnapshotManager = class {
|
|
|
1272
1295
|
}
|
|
1273
1296
|
async persistToFile(filePath, snapshotBaseDir, maxEntries) {
|
|
1274
1297
|
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1275
|
-
const tempPath =
|
|
1276
|
-
import_node_path.default.dirname(targetPath),
|
|
1277
|
-
`.layercache-${process.pid}-${Date.now()}-${(0, import_node_crypto.randomBytes)(8).toString("hex")}.tmp`
|
|
1278
|
-
);
|
|
1298
|
+
const tempPath = atomicWriteTempPath(targetPath);
|
|
1279
1299
|
let handle;
|
|
1280
1300
|
try {
|
|
1281
1301
|
handle = await import_node_fs.promises.open(tempPath, "wx");
|
|
@@ -1290,7 +1310,7 @@ var CacheStackSnapshotManager = class {
|
|
|
1290
1310
|
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
1291
1311
|
await openedHandle.close();
|
|
1292
1312
|
handle = void 0;
|
|
1293
|
-
await
|
|
1313
|
+
await commitAtomicWrite(tempPath, targetPath);
|
|
1294
1314
|
} catch (error) {
|
|
1295
1315
|
await handle?.close().catch(() => void 0);
|
|
1296
1316
|
await import_node_fs.promises.unlink(tempPath).catch(() => void 0);
|
|
@@ -1605,6 +1625,7 @@ var CircuitBreakerManager = class {
|
|
|
1605
1625
|
|
|
1606
1626
|
// src/internal/FetchRateLimiter.ts
|
|
1607
1627
|
var MAX_BUCKETS = 1e4;
|
|
1628
|
+
var MAX_QUEUE_PER_BUCKET = 1e4;
|
|
1608
1629
|
var FetchRateLimiter = class {
|
|
1609
1630
|
buckets = /* @__PURE__ */ new Map();
|
|
1610
1631
|
queuesByBucket = /* @__PURE__ */ new Map();
|
|
@@ -1613,6 +1634,7 @@ var FetchRateLimiter = class {
|
|
|
1613
1634
|
nextFetcherBucketId = 0;
|
|
1614
1635
|
drainTimer;
|
|
1615
1636
|
isDisposed = false;
|
|
1637
|
+
rateLimitBypasses = 0;
|
|
1616
1638
|
async schedule(options, context, task) {
|
|
1617
1639
|
if (this.isDisposed) {
|
|
1618
1640
|
throw new Error("FetchRateLimiter has been disposed.");
|
|
@@ -1627,6 +1649,11 @@ var FetchRateLimiter = class {
|
|
|
1627
1649
|
return new Promise((resolve2, reject) => {
|
|
1628
1650
|
const bucketKey = this.resolveBucketKey(normalized, context);
|
|
1629
1651
|
const queue = this.queuesByBucket.get(bucketKey) ?? [];
|
|
1652
|
+
if (queue.length >= MAX_QUEUE_PER_BUCKET) {
|
|
1653
|
+
this.rateLimitBypasses += 1;
|
|
1654
|
+
task().then(resolve2, reject);
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1630
1657
|
queue.push({
|
|
1631
1658
|
bucketKey,
|
|
1632
1659
|
options: normalized,
|
|
@@ -1931,7 +1958,13 @@ var MetricsCollector = class {
|
|
|
1931
1958
|
};
|
|
1932
1959
|
|
|
1933
1960
|
// src/internal/TtlResolver.ts
|
|
1961
|
+
var import_node_crypto2 = require("crypto");
|
|
1934
1962
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
1963
|
+
var secureRandom = {
|
|
1964
|
+
value() {
|
|
1965
|
+
return (0, import_node_crypto2.randomBytes)(4).readUInt32BE(0) / 4294967296;
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1935
1968
|
var TtlResolver = class {
|
|
1936
1969
|
accessProfiles = /* @__PURE__ */ new Map();
|
|
1937
1970
|
maxProfileEntries;
|
|
@@ -1994,7 +2027,7 @@ var TtlResolver = class {
|
|
|
1994
2027
|
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
1995
2028
|
return ttl;
|
|
1996
2029
|
}
|
|
1997
|
-
const delta = (
|
|
2030
|
+
const delta = (secureRandom.value() * 2 - 1) * jitter;
|
|
1998
2031
|
return Math.max(1, Math.round(ttl + delta));
|
|
1999
2032
|
}
|
|
2000
2033
|
resolvePolicyTtl(key, value, policy) {
|
|
@@ -2046,7 +2079,7 @@ var MAX_PATTERN_RECURSION_DEPTH = 500;
|
|
|
2046
2079
|
var TagIndex = class {
|
|
2047
2080
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
2048
2081
|
keyToTags = /* @__PURE__ */ new Map();
|
|
2049
|
-
knownKeys = /* @__PURE__ */ new
|
|
2082
|
+
knownKeys = /* @__PURE__ */ new Map();
|
|
2050
2083
|
maxKnownKeys;
|
|
2051
2084
|
nextNodeId = 1;
|
|
2052
2085
|
root = this.createTrieNode();
|
|
@@ -2134,10 +2167,11 @@ var TagIndex = class {
|
|
|
2134
2167
|
};
|
|
2135
2168
|
}
|
|
2136
2169
|
insertKnownKey(key) {
|
|
2137
|
-
|
|
2170
|
+
const isNew = !this.knownKeys.has(key);
|
|
2171
|
+
this.knownKeys.set(key, Date.now());
|
|
2172
|
+
if (!isNew) {
|
|
2138
2173
|
return;
|
|
2139
2174
|
}
|
|
2140
|
-
this.knownKeys.add(key);
|
|
2141
2175
|
let node = this.root;
|
|
2142
2176
|
for (const character of key) {
|
|
2143
2177
|
let child = node.children.get(character);
|
|
@@ -2232,14 +2266,13 @@ var TagIndex = class {
|
|
|
2232
2266
|
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
2233
2267
|
return;
|
|
2234
2268
|
}
|
|
2269
|
+
const sorted = [...this.knownKeys.entries()].sort((a, b) => a[1] - b[1]);
|
|
2235
2270
|
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
2236
|
-
let
|
|
2237
|
-
|
|
2238
|
-
if (
|
|
2239
|
-
|
|
2271
|
+
for (let i = 0; i < toRemove && i < sorted.length; i += 1) {
|
|
2272
|
+
const entry = sorted[i];
|
|
2273
|
+
if (entry) {
|
|
2274
|
+
this.removeKey(entry[0]);
|
|
2240
2275
|
}
|
|
2241
|
-
this.removeKey(key);
|
|
2242
|
-
removed += 1;
|
|
2243
2276
|
}
|
|
2244
2277
|
}
|
|
2245
2278
|
removeKey(key) {
|
|
@@ -2264,19 +2297,19 @@ var TagIndex = class {
|
|
|
2264
2297
|
if (!this.knownKeys.delete(key)) {
|
|
2265
2298
|
return;
|
|
2266
2299
|
}
|
|
2267
|
-
const
|
|
2300
|
+
const path = [];
|
|
2268
2301
|
let node = this.root;
|
|
2269
2302
|
for (const character of key) {
|
|
2270
2303
|
const child = node.children.get(character);
|
|
2271
2304
|
if (!child) {
|
|
2272
2305
|
return;
|
|
2273
2306
|
}
|
|
2274
|
-
|
|
2307
|
+
path.push([node, character]);
|
|
2275
2308
|
node = child;
|
|
2276
2309
|
}
|
|
2277
2310
|
node.terminal = false;
|
|
2278
|
-
for (let index =
|
|
2279
|
-
const entry =
|
|
2311
|
+
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
2312
|
+
const entry = path[index];
|
|
2280
2313
|
if (!entry) {
|
|
2281
2314
|
continue;
|
|
2282
2315
|
}
|
|
@@ -3359,7 +3392,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3359
3392
|
} catch (error) {
|
|
3360
3393
|
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3361
3394
|
this.metricsCollector.increment("refreshErrors");
|
|
3362
|
-
this.logger.
|
|
3395
|
+
this.logger.warn?.("background-refresh-error", { key, error: this.formatError(error) });
|
|
3363
3396
|
} finally {
|
|
3364
3397
|
this.backgroundRefreshes.delete(key);
|
|
3365
3398
|
this.backgroundRefreshAbort.delete(key);
|
|
@@ -3657,7 +3690,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3657
3690
|
}
|
|
3658
3691
|
}
|
|
3659
3692
|
shouldSkipLayer(layer) {
|
|
3660
|
-
|
|
3693
|
+
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
3694
|
+
const skip = shouldSkipLayer(degradedUntil);
|
|
3695
|
+
if (!skip && degradedUntil !== void 0) {
|
|
3696
|
+
this.layerDegradedUntil.delete(layer.name);
|
|
3697
|
+
}
|
|
3698
|
+
return skip;
|
|
3661
3699
|
}
|
|
3662
3700
|
async handleLayerFailure(layer, operation, error) {
|
|
3663
3701
|
const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
|
|
@@ -4872,12 +4910,12 @@ var RedisLayer = class {
|
|
|
4872
4910
|
};
|
|
4873
4911
|
|
|
4874
4912
|
// src/layers/DiskLayer.ts
|
|
4875
|
-
var
|
|
4913
|
+
var import_node_crypto4 = require("crypto");
|
|
4876
4914
|
var import_node_fs2 = require("fs");
|
|
4877
|
-
var
|
|
4915
|
+
var import_node_path = require("path");
|
|
4878
4916
|
|
|
4879
4917
|
// src/internal/PayloadProtection.ts
|
|
4880
|
-
var
|
|
4918
|
+
var import_node_crypto3 = require("crypto");
|
|
4881
4919
|
var MAGIC_ENCRYPTED = Buffer.from("LCP1:");
|
|
4882
4920
|
var MAGIC_SIGNED = Buffer.from("LCS1:");
|
|
4883
4921
|
var ALGORITHM = "aes-256-gcm";
|
|
@@ -4890,11 +4928,11 @@ var PayloadProtection = class {
|
|
|
4890
4928
|
constructor(options) {
|
|
4891
4929
|
if (options.encryptionKey) {
|
|
4892
4930
|
const raw = Buffer.isBuffer(options.encryptionKey) ? options.encryptionKey : Buffer.from(options.encryptionKey, "utf8");
|
|
4893
|
-
this.encryptionKey = (0,
|
|
4931
|
+
this.encryptionKey = (0, import_node_crypto3.createHash)("sha256").update(raw).digest();
|
|
4894
4932
|
}
|
|
4895
4933
|
if (options.signingKey && !options.encryptionKey) {
|
|
4896
4934
|
const raw = Buffer.isBuffer(options.signingKey) ? options.signingKey : Buffer.from(options.signingKey, "utf8");
|
|
4897
|
-
this.signingKey = (0,
|
|
4935
|
+
this.signingKey = (0, import_node_crypto3.createHash)("sha256").update(raw).digest();
|
|
4898
4936
|
}
|
|
4899
4937
|
}
|
|
4900
4938
|
/** Returns `true` when any protection (encryption or signing) is configured. */
|
|
@@ -4941,8 +4979,8 @@ var PayloadProtection = class {
|
|
|
4941
4979
|
}
|
|
4942
4980
|
// ── Encryption (AES-256-GCM) ──────────────────────────────────────────
|
|
4943
4981
|
encrypt(plaintext, key) {
|
|
4944
|
-
const iv = (0,
|
|
4945
|
-
const cipher = (0,
|
|
4982
|
+
const iv = (0, import_node_crypto3.randomBytes)(IV_LENGTH);
|
|
4983
|
+
const cipher = (0, import_node_crypto3.createCipheriv)(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
4946
4984
|
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
4947
4985
|
const authTag = cipher.getAuthTag();
|
|
4948
4986
|
return Buffer.concat([MAGIC_ENCRYPTED, iv, authTag, encrypted]);
|
|
@@ -4953,7 +4991,7 @@ var PayloadProtection = class {
|
|
|
4953
4991
|
const authTag = payload.subarray(headerEnd + IV_LENGTH, headerEnd + IV_LENGTH + AUTH_TAG_LENGTH);
|
|
4954
4992
|
const ciphertext = payload.subarray(headerEnd + IV_LENGTH + AUTH_TAG_LENGTH);
|
|
4955
4993
|
try {
|
|
4956
|
-
const decipher = (0,
|
|
4994
|
+
const decipher = (0, import_node_crypto3.createDecipheriv)(ALGORITHM, key, iv, {
|
|
4957
4995
|
authTagLength: AUTH_TAG_LENGTH
|
|
4958
4996
|
});
|
|
4959
4997
|
decipher.setAuthTag(authTag);
|
|
@@ -4966,15 +5004,15 @@ var PayloadProtection = class {
|
|
|
4966
5004
|
}
|
|
4967
5005
|
// ── Signing (HMAC-SHA256) ─────────────────────────────────────────────
|
|
4968
5006
|
sign(payload, key) {
|
|
4969
|
-
const hmac = (0,
|
|
5007
|
+
const hmac = (0, import_node_crypto3.createHmac)("sha256", key).update(payload).digest();
|
|
4970
5008
|
return Buffer.concat([MAGIC_SIGNED, hmac, payload]);
|
|
4971
5009
|
}
|
|
4972
5010
|
verify(payload, key) {
|
|
4973
5011
|
const headerEnd = MAGIC_SIGNED.length;
|
|
4974
5012
|
const receivedHmac = payload.subarray(headerEnd, headerEnd + HMAC_LENGTH);
|
|
4975
5013
|
const data = payload.subarray(headerEnd + HMAC_LENGTH);
|
|
4976
|
-
const expectedHmac = (0,
|
|
4977
|
-
if (receivedHmac.length !== HMAC_LENGTH || !(0,
|
|
5014
|
+
const expectedHmac = (0, import_node_crypto3.createHmac)("sha256", key).update(data).digest();
|
|
5015
|
+
if (receivedHmac.length !== HMAC_LENGTH || !(0, import_node_crypto3.timingSafeEqual)(receivedHmac, expectedHmac)) {
|
|
4978
5016
|
throw new PayloadProtectionError(
|
|
4979
5017
|
"HMAC verification failed. The data may have been tampered with or the signingKey is incorrect."
|
|
4980
5018
|
);
|
|
@@ -5054,7 +5092,7 @@ var DiskLayer = class {
|
|
|
5054
5092
|
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
|
|
5055
5093
|
const protectedPayload = this.protection.protect(raw);
|
|
5056
5094
|
const targetPath = this.keyToPath(key);
|
|
5057
|
-
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${(0,
|
|
5095
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${(0, import_node_crypto4.randomBytes)(8).toString("hex")}.tmp`;
|
|
5058
5096
|
try {
|
|
5059
5097
|
await import_node_fs2.promises.writeFile(tempPath, protectedPayload);
|
|
5060
5098
|
await import_node_fs2.promises.rename(tempPath, targetPath);
|
|
@@ -5116,7 +5154,7 @@ var DiskLayer = class {
|
|
|
5116
5154
|
return;
|
|
5117
5155
|
}
|
|
5118
5156
|
await this.deletePathsWithConcurrency(
|
|
5119
|
-
entries.filter((name) => name.endsWith(".lc")).map((name) => (0,
|
|
5157
|
+
entries.filter((name) => name.endsWith(".lc")).map((name) => (0, import_node_path.join)(this.directory, name))
|
|
5120
5158
|
);
|
|
5121
5159
|
});
|
|
5122
5160
|
}
|
|
@@ -5154,8 +5192,8 @@ var DiskLayer = class {
|
|
|
5154
5192
|
async dispose() {
|
|
5155
5193
|
}
|
|
5156
5194
|
keyToPath(key) {
|
|
5157
|
-
const hash = (0,
|
|
5158
|
-
return (0,
|
|
5195
|
+
const hash = (0, import_node_crypto4.createHash)("sha256").update(key).digest("hex");
|
|
5196
|
+
return (0, import_node_path.join)(this.directory, `${hash}.lc`);
|
|
5159
5197
|
}
|
|
5160
5198
|
resolveDirectory(directory) {
|
|
5161
5199
|
if (typeof directory !== "string" || directory.trim().length === 0) {
|
|
@@ -5164,7 +5202,7 @@ var DiskLayer = class {
|
|
|
5164
5202
|
if (directory.includes("\0")) {
|
|
5165
5203
|
throw new Error("DiskLayer.directory must not contain null bytes.");
|
|
5166
5204
|
}
|
|
5167
|
-
return (0,
|
|
5205
|
+
return (0, import_node_path.resolve)(directory);
|
|
5168
5206
|
}
|
|
5169
5207
|
normalizeMaxFiles(maxFiles) {
|
|
5170
5208
|
if (maxFiles === void 0) {
|
|
@@ -5247,7 +5285,7 @@ var DiskLayer = class {
|
|
|
5247
5285
|
if (name === void 0) {
|
|
5248
5286
|
return;
|
|
5249
5287
|
}
|
|
5250
|
-
const filePath = (0,
|
|
5288
|
+
const filePath = (0, import_node_path.join)(this.directory, name);
|
|
5251
5289
|
const raw = await this.readEntryFile(filePath);
|
|
5252
5290
|
if (raw === null) {
|
|
5253
5291
|
continue;
|
|
@@ -5323,7 +5361,7 @@ var DiskLayer = class {
|
|
|
5323
5361
|
}
|
|
5324
5362
|
const withStats = await Promise.all(
|
|
5325
5363
|
lcFiles.map(async (name) => {
|
|
5326
|
-
const filePath = (0,
|
|
5364
|
+
const filePath = (0, import_node_path.join)(this.directory, name);
|
|
5327
5365
|
try {
|
|
5328
5366
|
const stat = await import_node_fs2.promises.stat(filePath);
|
|
5329
5367
|
return { filePath, mtimeMs: stat.mtimeMs };
|
|
@@ -5439,7 +5477,7 @@ var MsgpackSerializer = class {
|
|
|
5439
5477
|
};
|
|
5440
5478
|
|
|
5441
5479
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
5442
|
-
var
|
|
5480
|
+
var import_node_crypto5 = require("crypto");
|
|
5443
5481
|
var RELEASE_SCRIPT = `
|
|
5444
5482
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
5445
5483
|
return redis.call("del", KEYS[1])
|
|
@@ -5463,7 +5501,7 @@ var RedisSingleFlightCoordinator = class {
|
|
|
5463
5501
|
}
|
|
5464
5502
|
async execute(key, options, worker, waiter) {
|
|
5465
5503
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
5466
|
-
const token = (0,
|
|
5504
|
+
const token = (0, import_node_crypto5.randomUUID)();
|
|
5467
5505
|
const acquired = await this.runCommand(
|
|
5468
5506
|
`acquire("${key}")`,
|
|
5469
5507
|
() => this.client.set(lockKey, token, "PX", options.leaseMs, "NX")
|