@kanadego/dsh-heartbeat 1.9.3 → 2.0.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/LICENSE +21 -21
- package/README.md +103 -110
- package/client.js +12 -0
- package/config/policy.json +74 -74
- package/config/profile-schema.json +306 -306
- package/dist/{bindings-225SI7KC.js → bindings-U2M3CO2D.js} +3 -3
- package/dist/{chunk-TY6UP74X.js → chunk-3DNGY4DS.js} +2 -2
- package/dist/{chunk-CUOSICYJ.js → chunk-544QOJN5.js} +397 -23
- package/dist/chunk-544QOJN5.js.map +1 -0
- package/dist/{chunk-TRO4E6WG.js → chunk-6MVKWICX.js} +587 -139
- package/dist/chunk-6MVKWICX.js.map +1 -0
- package/dist/{chunk-WLZSABBH.js → chunk-6XM6KY7M.js} +27 -10
- package/dist/chunk-6XM6KY7M.js.map +1 -0
- package/dist/chunk-AKIJ4LS2.js +680 -0
- package/dist/chunk-AKIJ4LS2.js.map +1 -0
- package/dist/{chunk-KB5SMG3F.js → chunk-DC5HMCJC.js} +3 -3
- package/dist/{chunk-3MDBU6WY.js → chunk-DZ7YCFGX.js} +37 -7
- package/dist/chunk-DZ7YCFGX.js.map +1 -0
- package/dist/{chunk-WRUTATW4.js → chunk-IBBAISSW.js} +13 -2
- package/dist/chunk-IBBAISSW.js.map +1 -0
- package/dist/{chunk-XYYEJK3W.js → chunk-IFRMKVIC.js} +82 -21
- package/dist/chunk-IFRMKVIC.js.map +1 -0
- package/dist/{chunk-XFNCTOW7.js → chunk-NCPKOWS7.js} +52 -4
- package/dist/chunk-NCPKOWS7.js.map +1 -0
- package/dist/{chunk-JDH2LDFS.js → chunk-TVX4HV4Y.js} +4 -6
- package/dist/{chunk-JDH2LDFS.js.map → chunk-TVX4HV4Y.js.map} +1 -1
- package/dist/cli/index.js +149 -26
- package/dist/cli/index.js.map +1 -1
- package/dist/{inbox-P26DDMWC.js → inbox-OWBK6PE2.js} +8 -7
- package/dist/index.d.ts +48 -315
- package/dist/index.js +259 -918
- package/dist/index.js.map +1 -1
- package/dist/migrate-TQ5PB2DI.js +27 -0
- package/dist/{orchestrator-ZPRGVDCB.js → orchestrator-P7AEWVYR.js} +23 -12
- package/dist/{pool-UTXQJJ5C.js → pool-2T6TVS2L.js} +4 -5
- package/dist/{snapshot-K5VHETTK.js → snapshot-F5B2DD56.js} +5 -5
- package/package.json +84 -83
- package/dist/chunk-3MDBU6WY.js.map +0 -1
- package/dist/chunk-CUOSICYJ.js.map +0 -1
- package/dist/chunk-IV2ZWQA3.js +0 -56
- package/dist/chunk-IV2ZWQA3.js.map +0 -1
- package/dist/chunk-K5Y6JP2B.js +0 -399
- package/dist/chunk-K5Y6JP2B.js.map +0 -1
- package/dist/chunk-TRO4E6WG.js.map +0 -1
- package/dist/chunk-WLZSABBH.js.map +0 -1
- package/dist/chunk-WRUTATW4.js.map +0 -1
- package/dist/chunk-XFNCTOW7.js.map +0 -1
- package/dist/chunk-XYYEJK3W.js.map +0 -1
- package/dist/migrate-D4VKAKYT.js +0 -21
- /package/dist/{bindings-225SI7KC.js.map → bindings-U2M3CO2D.js.map} +0 -0
- /package/dist/{chunk-TY6UP74X.js.map → chunk-3DNGY4DS.js.map} +0 -0
- /package/dist/{chunk-KB5SMG3F.js.map → chunk-DC5HMCJC.js.map} +0 -0
- /package/dist/{inbox-P26DDMWC.js.map → inbox-OWBK6PE2.js.map} +0 -0
- /package/dist/{migrate-D4VKAKYT.js.map → migrate-TQ5PB2DI.js.map} +0 -0
- /package/dist/{orchestrator-ZPRGVDCB.js.map → orchestrator-P7AEWVYR.js.map} +0 -0
- /package/dist/{pool-UTXQJJ5C.js.map → pool-2T6TVS2L.js.map} +0 -0
- /package/dist/{snapshot-K5VHETTK.js.map → snapshot-F5B2DD56.js.map} +0 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import {
|
|
2
|
+
burnFileSync
|
|
3
|
+
} from "./chunk-IBBAISSW.js";
|
|
4
|
+
|
|
5
|
+
// src/core/paths.ts
|
|
6
|
+
import fs2 from "fs";
|
|
7
|
+
import os from "os";
|
|
8
|
+
import path2 from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
|
|
11
|
+
// src/core/data-relocate.ts
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { createHash } from "crypto";
|
|
15
|
+
var SKELETON_DIRS = ["settings", "logs", "tmp", "exports"];
|
|
16
|
+
var SCRATCH_DIR = "tmp";
|
|
17
|
+
var MAGIC_V1 = Buffer.from("KHBV1", "ascii");
|
|
18
|
+
var MAGIC_V2 = Buffer.from("KHBV2", "ascii");
|
|
19
|
+
function hasPayload(dir) {
|
|
20
|
+
if (!fs.existsSync(dir)) return false;
|
|
21
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
22
|
+
if (!entry.isDirectory()) return true;
|
|
23
|
+
if (!SKELETON_DIRS.includes(entry.name)) return true;
|
|
24
|
+
if (entry.name === SCRATCH_DIR) continue;
|
|
25
|
+
if (fs.readdirSync(path.join(dir, entry.name)).length > 0) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function walk(dir, prefix = "", out = []) {
|
|
30
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
31
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
if (!prefix && entry.name === SCRATCH_DIR) continue;
|
|
34
|
+
walk(path.join(dir, entry.name), rel, out);
|
|
35
|
+
} else if (entry.isFile()) {
|
|
36
|
+
out.push(rel);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out.sort();
|
|
40
|
+
}
|
|
41
|
+
function copyTree(src, dst) {
|
|
42
|
+
const scratch = path.resolve(path.join(src, SCRATCH_DIR));
|
|
43
|
+
fs.cpSync(src, dst, {
|
|
44
|
+
recursive: true,
|
|
45
|
+
force: true,
|
|
46
|
+
preserveTimestamps: true,
|
|
47
|
+
filter: (from) => path.resolve(from) !== scratch
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function removeTree(dir) {
|
|
51
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
function sha256(file) {
|
|
54
|
+
return createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
55
|
+
}
|
|
56
|
+
function isEncryptedFile(file) {
|
|
57
|
+
const fd = fs.openSync(file, "r");
|
|
58
|
+
try {
|
|
59
|
+
const head = Buffer.alloc(MAGIC_V2.length);
|
|
60
|
+
const read = fs.readSync(fd, head, 0, head.length, 0);
|
|
61
|
+
if (read === MAGIC_V1.length && head.subarray(0, MAGIC_V1.length).equals(MAGIC_V1)) return true;
|
|
62
|
+
return read === MAGIC_V2.length && head.equals(MAGIC_V2);
|
|
63
|
+
} finally {
|
|
64
|
+
fs.closeSync(fd);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function parsesAsJson(file) {
|
|
68
|
+
if (isEncryptedFile(file)) return true;
|
|
69
|
+
const text = fs.readFileSync(file, "utf8");
|
|
70
|
+
try {
|
|
71
|
+
if (file.endsWith(".jsonl")) {
|
|
72
|
+
for (const line of text.split("\n")) if (line.trim()) JSON.parse(line);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
JSON.parse(text);
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function verifyTrees(srcDir, dstDir) {
|
|
82
|
+
const problems = [];
|
|
83
|
+
const warnings = [];
|
|
84
|
+
let files = 0;
|
|
85
|
+
let bytes = 0;
|
|
86
|
+
if (!fs.existsSync(dstDir)) {
|
|
87
|
+
return { ok: false, files: 0, bytes: 0, problems: [`copy missing: ${dstDir}`], warnings };
|
|
88
|
+
}
|
|
89
|
+
const expected = walk(srcDir);
|
|
90
|
+
const actual = walk(dstDir);
|
|
91
|
+
const actualSet = new Set(actual);
|
|
92
|
+
const expectedSet = new Set(expected);
|
|
93
|
+
for (const rel of expected) if (!actualSet.has(rel)) problems.push(`missing: ${rel}`);
|
|
94
|
+
for (const rel of actual) if (!expectedSet.has(rel)) problems.push(`unexpected: ${rel}`);
|
|
95
|
+
for (const rel of expected) {
|
|
96
|
+
const from = path.join(srcDir, rel);
|
|
97
|
+
const to = path.join(dstDir, rel);
|
|
98
|
+
if (!fs.existsSync(to)) continue;
|
|
99
|
+
const sizeFrom = fs.statSync(from).size;
|
|
100
|
+
const sizeTo = fs.statSync(to).size;
|
|
101
|
+
files += 1;
|
|
102
|
+
bytes += sizeFrom;
|
|
103
|
+
if (sizeFrom !== sizeTo) {
|
|
104
|
+
problems.push(`size differs: ${rel} (${sizeFrom} -> ${sizeTo})`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (sha256(from) !== sha256(to)) {
|
|
108
|
+
problems.push(`content differs: ${rel}`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (/\.(json|jsonl)$/i.test(rel) && !parsesAsJson(to)) warnings.push(`unparseable: ${rel}`);
|
|
112
|
+
}
|
|
113
|
+
return { ok: problems.length === 0, files, bytes, problems, warnings };
|
|
114
|
+
}
|
|
115
|
+
function stamp(now) {
|
|
116
|
+
const d = new Date(now);
|
|
117
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
118
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
119
|
+
}
|
|
120
|
+
function errorText(error) {
|
|
121
|
+
return error instanceof Error ? error.message : String(error);
|
|
122
|
+
}
|
|
123
|
+
function relocateDataDir(opts) {
|
|
124
|
+
const srcDir = path.resolve(opts.srcDir);
|
|
125
|
+
const dstDir = path.resolve(opts.dstDir);
|
|
126
|
+
if (!fs.existsSync(srcDir)) return { action: "skipped", reason: "source-missing" };
|
|
127
|
+
if (!hasPayload(srcDir)) return { action: "skipped", reason: "no-payload" };
|
|
128
|
+
if (hasPayload(dstDir)) return { action: "skipped", reason: "destination-exists" };
|
|
129
|
+
const suffix = stamp(opts.now ?? Date.now());
|
|
130
|
+
const backupDir = `${srcDir}.bak-relocate-${suffix}`;
|
|
131
|
+
const stagingDir = `${dstDir}.staging-${suffix}`;
|
|
132
|
+
try {
|
|
133
|
+
copyTree(srcDir, backupDir);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return { action: "failed", stage: "backup", problems: [errorText(error)], backupDir: null };
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
copyTree(srcDir, stagingDir);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return { action: "failed", stage: "copy", problems: [errorText(error)], backupDir };
|
|
141
|
+
}
|
|
142
|
+
const verified = verifyTrees(srcDir, stagingDir);
|
|
143
|
+
if (!verified.ok) {
|
|
144
|
+
try {
|
|
145
|
+
removeTree(stagingDir);
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
return { action: "failed", stage: "verify", problems: verified.problems, backupDir };
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
if (fs.existsSync(dstDir)) removeTree(dstDir);
|
|
152
|
+
fs.renameSync(stagingDir, dstDir);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
try {
|
|
155
|
+
removeTree(stagingDir);
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
return { action: "failed", stage: "commit", problems: [errorText(error)], backupDir };
|
|
159
|
+
}
|
|
160
|
+
const leftovers = [];
|
|
161
|
+
try {
|
|
162
|
+
removeTree(srcDir);
|
|
163
|
+
} catch {
|
|
164
|
+
leftovers.push(srcDir);
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
removeTree(backupDir);
|
|
168
|
+
} catch {
|
|
169
|
+
leftovers.push(backupDir);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
action: "relocated",
|
|
173
|
+
srcDir,
|
|
174
|
+
dstDir,
|
|
175
|
+
backupDir,
|
|
176
|
+
files: verified.files,
|
|
177
|
+
bytes: verified.bytes,
|
|
178
|
+
warnings: verified.warnings,
|
|
179
|
+
leftovers
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/core/paths.ts
|
|
184
|
+
var resolved = null;
|
|
185
|
+
function findPackageRoot(startFile) {
|
|
186
|
+
let dir = path2.dirname(path2.resolve(startFile));
|
|
187
|
+
for (; ; ) {
|
|
188
|
+
if (fs2.existsSync(path2.join(dir, "package.json"))) return dir;
|
|
189
|
+
const parent = path2.dirname(dir);
|
|
190
|
+
if (parent === dir) throw new Error("package.json not found above " + startFile);
|
|
191
|
+
dir = parent;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function packageRoot() {
|
|
195
|
+
return findPackageRoot(fileURLToPath(import.meta.url));
|
|
196
|
+
}
|
|
197
|
+
function defaultDataDir(env = process.env, home = os.homedir()) {
|
|
198
|
+
return path2.join(dshHomeDir(env, home), "heartbeat-data");
|
|
199
|
+
}
|
|
200
|
+
function dshHomeDir(env = process.env, home = os.homedir()) {
|
|
201
|
+
const override = env.DSH_HOME?.trim();
|
|
202
|
+
return override && override.length > 0 ? override : path2.join(home, ".dsh");
|
|
203
|
+
}
|
|
204
|
+
function initWorkspace(config = {}) {
|
|
205
|
+
if (resolved) return resolved;
|
|
206
|
+
const root = packageRoot();
|
|
207
|
+
const explicit = process.env.HEARTBEAT_DATA_DIR || config.dataDir;
|
|
208
|
+
const homeDir = path2.resolve(defaultDataDir());
|
|
209
|
+
const legacyDir = path2.join(root, "data");
|
|
210
|
+
let dataDir;
|
|
211
|
+
let relocation;
|
|
212
|
+
if (explicit) {
|
|
213
|
+
dataDir = path2.resolve(explicit);
|
|
214
|
+
} else if (hasPayload(homeDir)) {
|
|
215
|
+
dataDir = homeDir;
|
|
216
|
+
} else if (hasPayload(legacyDir)) {
|
|
217
|
+
relocation = relocateDataDir({ srcDir: legacyDir, dstDir: homeDir });
|
|
218
|
+
dataDir = relocation.action === "relocated" ? homeDir : legacyDir;
|
|
219
|
+
} else {
|
|
220
|
+
dataDir = homeDir;
|
|
221
|
+
}
|
|
222
|
+
const paths = {
|
|
223
|
+
dataDir,
|
|
224
|
+
settingsDir: path2.join(dataDir, "settings"),
|
|
225
|
+
logsDir: path2.join(dataDir, "logs"),
|
|
226
|
+
tmpDir: path2.join(dataDir, "tmp"),
|
|
227
|
+
exportsDir: path2.join(dataDir, "exports"),
|
|
228
|
+
packageRoot: root,
|
|
229
|
+
configDir: path2.join(root, "config"),
|
|
230
|
+
assetsDir: path2.join(root, "assets"),
|
|
231
|
+
...relocation ? { relocation } : {}
|
|
232
|
+
};
|
|
233
|
+
for (const dir of [
|
|
234
|
+
paths.dataDir,
|
|
235
|
+
paths.settingsDir,
|
|
236
|
+
paths.logsDir,
|
|
237
|
+
paths.tmpDir,
|
|
238
|
+
paths.exportsDir
|
|
239
|
+
]) {
|
|
240
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
241
|
+
}
|
|
242
|
+
resolved = paths;
|
|
243
|
+
return paths;
|
|
244
|
+
}
|
|
245
|
+
function workspace() {
|
|
246
|
+
if (!resolved) throw new Error("workspace not initialized: call initWorkspace() first");
|
|
247
|
+
return resolved;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/core/file-lock.ts
|
|
251
|
+
import fs3 from "fs";
|
|
252
|
+
var STALE_MS = 15e3;
|
|
253
|
+
var WAIT_MS = 3e3;
|
|
254
|
+
var POLL_MS = 25;
|
|
255
|
+
var UNKNOWN_HOLDER_MS = 2 * STALE_MS;
|
|
256
|
+
var FileLockTimeoutError = class extends Error {
|
|
257
|
+
target;
|
|
258
|
+
waitedMs;
|
|
259
|
+
constructor(target, waitedMs) {
|
|
260
|
+
super(`file lock timeout after ${waitedMs}ms: ${target}`);
|
|
261
|
+
this.name = "FileLockTimeoutError";
|
|
262
|
+
this.target = target;
|
|
263
|
+
this.waitedMs = waitedMs;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
function sleepSync(ms) {
|
|
267
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
268
|
+
}
|
|
269
|
+
function holderAlive(lock, ageMs) {
|
|
270
|
+
let raw = "";
|
|
271
|
+
try {
|
|
272
|
+
raw = fs3.readFileSync(lock, "utf8");
|
|
273
|
+
} catch {
|
|
274
|
+
return ageMs < UNKNOWN_HOLDER_MS;
|
|
275
|
+
}
|
|
276
|
+
const pid = Number.parseInt(raw.trim().split(/\s+/)[0] ?? "", 10);
|
|
277
|
+
if (!Number.isInteger(pid) || pid <= 0) return ageMs < UNKNOWN_HOLDER_MS;
|
|
278
|
+
if (pid === process.pid) return true;
|
|
279
|
+
try {
|
|
280
|
+
process.kill(pid, 0);
|
|
281
|
+
return true;
|
|
282
|
+
} catch (e) {
|
|
283
|
+
return e.code === "EPERM";
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function tryAcquire(lock) {
|
|
287
|
+
try {
|
|
288
|
+
const fd = fs3.openSync(lock, "wx");
|
|
289
|
+
fs3.writeSync(fd, `${process.pid} ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
290
|
+
`);
|
|
291
|
+
fs3.closeSync(fd);
|
|
292
|
+
return true;
|
|
293
|
+
} catch (e) {
|
|
294
|
+
if (e.code !== "EEXIST") throw e;
|
|
295
|
+
try {
|
|
296
|
+
const ageMs = Date.now() - fs3.statSync(lock).mtimeMs;
|
|
297
|
+
if (ageMs > STALE_MS && !holderAlive(lock, ageMs)) {
|
|
298
|
+
fs3.rmSync(lock, { force: true });
|
|
299
|
+
return tryAcquire(lock);
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
return tryAcquire(lock);
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function acquire(lock, waitMs) {
|
|
308
|
+
const deadline = Date.now() + waitMs;
|
|
309
|
+
for (; ; ) {
|
|
310
|
+
if (tryAcquire(lock)) return true;
|
|
311
|
+
if (Date.now() >= deadline) return false;
|
|
312
|
+
sleepSync(POLL_MS);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function release(lock) {
|
|
316
|
+
try {
|
|
317
|
+
fs3.rmSync(lock, { force: true });
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function withFileLock(target, fn, opts = {}) {
|
|
322
|
+
const waitMs = opts.waitMs ?? WAIT_MS;
|
|
323
|
+
const lock = `${target}.lock`;
|
|
324
|
+
if (!acquire(lock, waitMs)) throw new FileLockTimeoutError(target, waitMs);
|
|
325
|
+
try {
|
|
326
|
+
return fn();
|
|
327
|
+
} finally {
|
|
328
|
+
release(lock);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function tryWithFileLock(target, fn, opts = {}) {
|
|
332
|
+
const waitMs = opts.waitMs ?? WAIT_MS;
|
|
333
|
+
const lock = `${target}.lock`;
|
|
334
|
+
if (!acquire(lock, waitMs)) return null;
|
|
335
|
+
try {
|
|
336
|
+
return fn();
|
|
337
|
+
} finally {
|
|
338
|
+
release(lock);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/core/fs-walk.ts
|
|
343
|
+
import fs4 from "fs";
|
|
344
|
+
import path3 from "path";
|
|
345
|
+
function matchFilesRecursive(root, match) {
|
|
346
|
+
const re = new RegExp(match.source, match.flags.replace(/g/g, ""));
|
|
347
|
+
const out = [];
|
|
348
|
+
const walk2 = (dir, prefix) => {
|
|
349
|
+
let entries;
|
|
350
|
+
try {
|
|
351
|
+
entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
352
|
+
} catch {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
for (const e of entries) {
|
|
356
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
357
|
+
if (e.isDirectory()) {
|
|
358
|
+
walk2(path3.join(dir, e.name), rel);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (!e.isFile()) continue;
|
|
362
|
+
if (re.test(e.name)) out.push(rel);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
walk2(root, "");
|
|
366
|
+
return out.sort();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/vault/vault.ts
|
|
370
|
+
import fs5 from "fs";
|
|
371
|
+
import path4 from "path";
|
|
372
|
+
import { spawnSync } from "child_process";
|
|
373
|
+
import { createCipheriv, createDecipheriv, randomBytes, randomUUID } from "crypto";
|
|
374
|
+
var MAGIC_V12 = Buffer.from("KHBV1", "ascii");
|
|
375
|
+
var MAGIC_V22 = Buffer.from("KHBV2", "ascii");
|
|
376
|
+
var V2_VERSION = 1;
|
|
377
|
+
var V2_NONCE_BYTES = 12;
|
|
378
|
+
var V2_TAG_BYTES = 16;
|
|
379
|
+
var V2_HEADER_BYTES = MAGIC_V22.length + 1 + V2_NONCE_BYTES + V2_TAG_BYTES;
|
|
380
|
+
var V2_AAD = Buffer.concat([MAGIC_V22, Buffer.from([V2_VERSION])]);
|
|
381
|
+
var KEY_BYTES = 32;
|
|
382
|
+
var KEY_FILE_NAME = "vault.key";
|
|
383
|
+
var VAULT_TIMEOUT_MS = 3e4;
|
|
384
|
+
var cachedScriptPath = null;
|
|
385
|
+
function vaultScriptPath() {
|
|
386
|
+
if (cachedScriptPath) return cachedScriptPath;
|
|
387
|
+
const script = path4.join(workspace().assetsDir, "vault.ps1");
|
|
388
|
+
if (!fs5.existsSync(script)) throw new Error(`vault script missing: ${script}`);
|
|
389
|
+
cachedScriptPath = script;
|
|
390
|
+
return script;
|
|
391
|
+
}
|
|
392
|
+
function tmpPlainName(target) {
|
|
393
|
+
return path4.join(workspace().tmpDir, `.${path4.basename(target)}.${randomUUID().slice(0, 8)}.plain`);
|
|
394
|
+
}
|
|
395
|
+
function tmpEncName(target) {
|
|
396
|
+
return path4.join(workspace().tmpDir, `.${path4.basename(target)}.${randomUUID().slice(0, 8)}.enc`);
|
|
397
|
+
}
|
|
398
|
+
function runVault(guard, args) {
|
|
399
|
+
const r = spawnSync(
|
|
400
|
+
"powershell.exe",
|
|
401
|
+
["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", vaultScriptPath(), ...args],
|
|
402
|
+
{ timeout: VAULT_TIMEOUT_MS, encoding: "utf8" }
|
|
403
|
+
);
|
|
404
|
+
if (r.error) throw new Error(`vault spawn failed: ${String(r.error)}`);
|
|
405
|
+
if (r.status !== 0) {
|
|
406
|
+
const detail = (r.stderr || r.stdout || "").trim().split("\n").slice(-3).join("; ");
|
|
407
|
+
throw new Error(`vault ${args[0]} failed (exit ${r.status}): ${detail}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
var SCRATCH_STALE_MS = 10 * 6e4;
|
|
411
|
+
var scratchSwept = false;
|
|
412
|
+
function sweepVaultScratch() {
|
|
413
|
+
if (scratchSwept) return 0;
|
|
414
|
+
scratchSwept = true;
|
|
415
|
+
let burned = 0;
|
|
416
|
+
try {
|
|
417
|
+
const dir = workspace().tmpDir;
|
|
418
|
+
for (const name of fs5.readdirSync(dir)) {
|
|
419
|
+
if (!/\.(plain|enc)$/.test(name)) continue;
|
|
420
|
+
const p = path4.join(dir, name);
|
|
421
|
+
try {
|
|
422
|
+
if (Date.now() - fs5.statSync(p).mtimeMs < SCRATCH_STALE_MS) continue;
|
|
423
|
+
} catch {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
burnFileSync(p);
|
|
427
|
+
burned += 1;
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
return burned;
|
|
432
|
+
}
|
|
433
|
+
function containerKind(file) {
|
|
434
|
+
if (!fs5.existsSync(file)) return "plain";
|
|
435
|
+
const fd = fs5.openSync(file, "r");
|
|
436
|
+
try {
|
|
437
|
+
const head = Buffer.alloc(MAGIC_V22.length);
|
|
438
|
+
const read = fs5.readSync(fd, head, 0, head.length, 0);
|
|
439
|
+
if (read === MAGIC_V12.length && head.subarray(0, MAGIC_V12.length).equals(MAGIC_V12)) return "v1";
|
|
440
|
+
if (read === MAGIC_V22.length && head.equals(MAGIC_V22)) return "v2";
|
|
441
|
+
return "plain";
|
|
442
|
+
} finally {
|
|
443
|
+
fs5.closeSync(fd);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function isEncrypted(file) {
|
|
447
|
+
return containerKind(file) !== "plain";
|
|
448
|
+
}
|
|
449
|
+
function encryptBlob(key, plain) {
|
|
450
|
+
const nonce = randomBytes(V2_NONCE_BYTES);
|
|
451
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
452
|
+
cipher.setAAD(V2_AAD);
|
|
453
|
+
const body = Buffer.concat([cipher.update(plain), cipher.final()]);
|
|
454
|
+
return Buffer.concat([MAGIC_V22, Buffer.from([V2_VERSION]), nonce, cipher.getAuthTag(), body]);
|
|
455
|
+
}
|
|
456
|
+
function decryptBlob(key, raw) {
|
|
457
|
+
if (raw.length < V2_HEADER_BYTES) throw new Error("vault: truncated KHBV2 container");
|
|
458
|
+
if (!raw.subarray(0, MAGIC_V22.length).equals(MAGIC_V22)) throw new Error("vault: missing KHBV2 header");
|
|
459
|
+
const version = raw[MAGIC_V22.length];
|
|
460
|
+
if (version !== V2_VERSION) throw new Error(`vault: unsupported KHBV2 version ${version}`);
|
|
461
|
+
const nonceAt = MAGIC_V22.length + 1;
|
|
462
|
+
const tagAt = nonceAt + V2_NONCE_BYTES;
|
|
463
|
+
const bodyAt = tagAt + V2_TAG_BYTES;
|
|
464
|
+
const decipher = createDecipheriv("aes-256-gcm", key, raw.subarray(nonceAt, tagAt));
|
|
465
|
+
decipher.setAAD(Buffer.concat([MAGIC_V22, Buffer.from([version])]));
|
|
466
|
+
decipher.setAuthTag(raw.subarray(tagAt, bodyAt));
|
|
467
|
+
return Buffer.concat([decipher.update(raw.subarray(bodyAt)), decipher.final()]);
|
|
468
|
+
}
|
|
469
|
+
function legacyUnprotect(guard, file, out) {
|
|
470
|
+
runVault(guard, ["unprotect", "-InFile", file, "-OutFile", out]);
|
|
471
|
+
}
|
|
472
|
+
function legacyProtect(guard, inFile, out) {
|
|
473
|
+
runVault(guard, ["protect", "-InFile", inFile, "-OutFile", out]);
|
|
474
|
+
}
|
|
475
|
+
function vaultKeyPath() {
|
|
476
|
+
return path4.join(workspace().dataDir, KEY_FILE_NAME);
|
|
477
|
+
}
|
|
478
|
+
var cachedMasterKey = null;
|
|
479
|
+
function decodeKeyFile(guard, file) {
|
|
480
|
+
const kind = containerKind(file);
|
|
481
|
+
if (kind === "v2") {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`vault: ${file} is an AES container, but the master key cannot be stored AES-encrypted \u2014 restore it from a backup or re-import the data`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (kind !== "v1") throw new Error(`vault: ${file} is not a DPAPI container (not a usable master key)`);
|
|
487
|
+
const tmp = tmpPlainName(file);
|
|
488
|
+
try {
|
|
489
|
+
legacyUnprotect(guard, file, tmp);
|
|
490
|
+
const key = fs5.readFileSync(tmp);
|
|
491
|
+
if (key.length !== KEY_BYTES) {
|
|
492
|
+
throw new Error(`vault: master key has ${key.length} bytes, expected ${KEY_BYTES}`);
|
|
493
|
+
}
|
|
494
|
+
return key;
|
|
495
|
+
} finally {
|
|
496
|
+
burnFileSync(tmp);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function masterKey(guard) {
|
|
500
|
+
if (cachedMasterKey) return cachedMasterKey;
|
|
501
|
+
sweepVaultScratch();
|
|
502
|
+
const file = guard.assert(vaultKeyPath());
|
|
503
|
+
if (fs5.existsSync(file)) {
|
|
504
|
+
cachedMasterKey = decodeKeyFile(guard, file);
|
|
505
|
+
return cachedMasterKey;
|
|
506
|
+
}
|
|
507
|
+
fs5.mkdirSync(path4.dirname(file), { recursive: true });
|
|
508
|
+
let created;
|
|
509
|
+
try {
|
|
510
|
+
created = withFileLock(file, () => {
|
|
511
|
+
if (fs5.existsSync(file)) return null;
|
|
512
|
+
const plain = tmpPlainName(file);
|
|
513
|
+
const enc = tmpEncName(file);
|
|
514
|
+
try {
|
|
515
|
+
const key = randomBytes(KEY_BYTES);
|
|
516
|
+
fs5.writeFileSync(plain, key);
|
|
517
|
+
legacyProtect(guard, plain, enc);
|
|
518
|
+
const check = tmpPlainName(file);
|
|
519
|
+
try {
|
|
520
|
+
legacyUnprotect(guard, enc, check);
|
|
521
|
+
if (!fs5.readFileSync(check).equals(key)) throw new Error("vault: master key round-trip mismatch");
|
|
522
|
+
} finally {
|
|
523
|
+
burnFileSync(check);
|
|
524
|
+
}
|
|
525
|
+
fs5.renameSync(enc, file);
|
|
526
|
+
return key;
|
|
527
|
+
} finally {
|
|
528
|
+
burnFileSync(plain);
|
|
529
|
+
fs5.rmSync(enc, { force: true });
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
} catch (e) {
|
|
533
|
+
if (e instanceof FileLockTimeoutError && fs5.existsSync(file)) {
|
|
534
|
+
cachedMasterKey = decodeKeyFile(guard, file);
|
|
535
|
+
return cachedMasterKey;
|
|
536
|
+
}
|
|
537
|
+
throw e;
|
|
538
|
+
}
|
|
539
|
+
cachedMasterKey = created ?? decodeKeyFile(guard, file);
|
|
540
|
+
return cachedMasterKey;
|
|
541
|
+
}
|
|
542
|
+
function readContainer(guard, file) {
|
|
543
|
+
const kind = containerKind(file);
|
|
544
|
+
if (kind === "v2") return decryptBlob(masterKey(guard), fs5.readFileSync(file));
|
|
545
|
+
if (kind === "v1") {
|
|
546
|
+
const tmp = tmpPlainName(file);
|
|
547
|
+
try {
|
|
548
|
+
legacyUnprotect(guard, file, tmp);
|
|
549
|
+
return fs5.readFileSync(tmp);
|
|
550
|
+
} finally {
|
|
551
|
+
burnFileSync(tmp);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return fs5.readFileSync(file);
|
|
555
|
+
}
|
|
556
|
+
function writeContainer(guard, file, data) {
|
|
557
|
+
const f = guard.assert(file);
|
|
558
|
+
fs5.mkdirSync(path4.dirname(f), { recursive: true });
|
|
559
|
+
const enc = tmpEncName(f);
|
|
560
|
+
try {
|
|
561
|
+
fs5.writeFileSync(enc, encryptBlob(masterKey(guard), data));
|
|
562
|
+
fs5.renameSync(enc, f);
|
|
563
|
+
} finally {
|
|
564
|
+
fs5.rmSync(enc, { force: true });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function loadJson(guard, file) {
|
|
568
|
+
const f = guard.assert(file);
|
|
569
|
+
if (!fs5.existsSync(f)) return null;
|
|
570
|
+
if (containerKind(f) === "plain") {
|
|
571
|
+
try {
|
|
572
|
+
return JSON.parse(fs5.readFileSync(f, "utf8"));
|
|
573
|
+
} catch {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return JSON.parse(readContainer(guard, f).toString("utf8"));
|
|
578
|
+
}
|
|
579
|
+
function saveJson(guard, file, value) {
|
|
580
|
+
writeContainer(guard, file, Buffer.from(JSON.stringify(value, null, 2), "utf8"));
|
|
581
|
+
}
|
|
582
|
+
function readText(guard, file, fallback = "") {
|
|
583
|
+
const f = guard.assert(file);
|
|
584
|
+
try {
|
|
585
|
+
return fs5.readFileSync(f, "utf8");
|
|
586
|
+
} catch {
|
|
587
|
+
return fallback;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function writeText(guard, file, content) {
|
|
591
|
+
const f = guard.assert(file);
|
|
592
|
+
fs5.mkdirSync(path4.dirname(f), { recursive: true });
|
|
593
|
+
const tmp = tmpPlainName(f);
|
|
594
|
+
try {
|
|
595
|
+
fs5.writeFileSync(tmp, content, "utf8");
|
|
596
|
+
fs5.renameSync(tmp, f);
|
|
597
|
+
} finally {
|
|
598
|
+
burnFileSync(tmp);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function loadEncryptedText(guard, file) {
|
|
602
|
+
const f = guard.assert(file);
|
|
603
|
+
if (!fs5.existsSync(f)) return null;
|
|
604
|
+
return readContainer(guard, f).toString("utf8");
|
|
605
|
+
}
|
|
606
|
+
function saveEncryptedText(guard, file, content) {
|
|
607
|
+
writeContainer(guard, file, Buffer.from(content, "utf8"));
|
|
608
|
+
}
|
|
609
|
+
function encryptFile(guard, inFile, outFile) {
|
|
610
|
+
const inPath = guard.assert(inFile);
|
|
611
|
+
writeContainer(guard, outFile, fs5.readFileSync(inPath));
|
|
612
|
+
}
|
|
613
|
+
function decryptFile(guard, inFile, outFile) {
|
|
614
|
+
const inPath = guard.assert(inFile);
|
|
615
|
+
const outPath = guard.assert(outFile);
|
|
616
|
+
fs5.mkdirSync(path4.dirname(outPath), { recursive: true });
|
|
617
|
+
const data = readContainer(guard, inPath);
|
|
618
|
+
const tmp = tmpPlainName(outPath);
|
|
619
|
+
try {
|
|
620
|
+
fs5.writeFileSync(tmp, data);
|
|
621
|
+
fs5.renameSync(tmp, outPath);
|
|
622
|
+
} finally {
|
|
623
|
+
burnFileSync(tmp);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function upgradeVaultFile(guard, file) {
|
|
627
|
+
const f = guard.assert(file);
|
|
628
|
+
if (containerKind(f) !== "v1") return false;
|
|
629
|
+
const data = readContainer(guard, f);
|
|
630
|
+
writeContainer(guard, f, data);
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
function upgradeVaultTree(guard, dataDir) {
|
|
634
|
+
const result = { scanned: 0, upgraded: 0, failed: [] };
|
|
635
|
+
for (const rel of matchFilesRecursive(dataDir, /./)) {
|
|
636
|
+
if (rel === KEY_FILE_NAME || rel.startsWith("tmp/") || rel.startsWith("tmp\\")) continue;
|
|
637
|
+
const abs = path4.join(dataDir, rel.split("/").join(path4.sep));
|
|
638
|
+
try {
|
|
639
|
+
result.scanned += 1;
|
|
640
|
+
if (upgradeVaultFile(guard, abs)) result.upgraded += 1;
|
|
641
|
+
} catch (e) {
|
|
642
|
+
result.failed.push({ file: rel, reason: e.message });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return result;
|
|
646
|
+
}
|
|
647
|
+
function upgradeOneLegacyVaultFile(guard, dataDir) {
|
|
648
|
+
for (const rel of matchFilesRecursive(dataDir, /./)) {
|
|
649
|
+
if (rel === KEY_FILE_NAME || rel.startsWith("tmp/") || rel.startsWith("tmp\\")) continue;
|
|
650
|
+
const abs = path4.join(dataDir, rel.split("/").join(path4.sep));
|
|
651
|
+
try {
|
|
652
|
+
if (upgradeVaultFile(guard, abs)) return rel;
|
|
653
|
+
} catch {
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export {
|
|
660
|
+
dshHomeDir,
|
|
661
|
+
initWorkspace,
|
|
662
|
+
workspace,
|
|
663
|
+
withFileLock,
|
|
664
|
+
tryWithFileLock,
|
|
665
|
+
matchFilesRecursive,
|
|
666
|
+
containerKind,
|
|
667
|
+
isEncrypted,
|
|
668
|
+
vaultKeyPath,
|
|
669
|
+
loadJson,
|
|
670
|
+
saveJson,
|
|
671
|
+
readText,
|
|
672
|
+
writeText,
|
|
673
|
+
loadEncryptedText,
|
|
674
|
+
saveEncryptedText,
|
|
675
|
+
encryptFile,
|
|
676
|
+
decryptFile,
|
|
677
|
+
upgradeVaultTree,
|
|
678
|
+
upgradeOneLegacyVaultFile
|
|
679
|
+
};
|
|
680
|
+
//# sourceMappingURL=chunk-AKIJ4LS2.js.map
|