@openclaw/fs-safe 0.2.3 → 0.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/CHANGELOG.md +21 -1
- package/README.md +2 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +12 -1
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +6 -5
- package/dist/json-document-store.d.ts +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +5 -3
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +157 -4
- package/dist/json-stringify.d.ts +2 -0
- package/dist/json-stringify.d.ts.map +1 -0
- package/dist/json-stringify.js +7 -0
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +3 -2
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -1
- package/dist/path.js +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +7 -10
- package/dist/pinned-python.d.ts.map +1 -1
- package/dist/pinned-python.js +125 -43
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +6 -2
- package/dist/root-paths.js +25 -3
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +3 -1
- package/dist/sidecar-lock.d.ts +8 -1
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +40 -7
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +8 -1
- package/docs/config.md +2 -2
- package/docs/index.md +1 -1
- package/docs/json-store.md +3 -1
- package/docs/sidecar-lock.md +33 -5
- package/package.json +4 -4
- package/dist/path-scope.d.ts +0 -2
- package/dist/path-scope.d.ts.map +0 -1
- package/dist/path-scope.js +0 -1
- package/dist/private-file-store.d.ts +0 -58
- package/dist/private-file-store.d.ts.map +0 -1
- package/dist/private-file-store.js +0 -220
package/dist/pinned-python.js
CHANGED
|
@@ -87,6 +87,85 @@ def reject_unsafe_endpoint(st):
|
|
|
87
87
|
if stat.S_ISREG(mode) and st.st_nlink > 1:
|
|
88
88
|
raise OSError(errno.EPERM, "hardlinked file endpoint is not allowed")
|
|
89
89
|
|
|
90
|
+
def copy_bytes(source_fd, dest_fd):
|
|
91
|
+
while True:
|
|
92
|
+
chunk = os.read(source_fd, 65536)
|
|
93
|
+
if not chunk:
|
|
94
|
+
break
|
|
95
|
+
view = memoryview(chunk)
|
|
96
|
+
while view:
|
|
97
|
+
written = os.write(dest_fd, view)
|
|
98
|
+
if written <= 0:
|
|
99
|
+
raise OSError(errno.EIO, "short write")
|
|
100
|
+
view = view[written:]
|
|
101
|
+
|
|
102
|
+
def write_all(fd, data):
|
|
103
|
+
view = memoryview(data)
|
|
104
|
+
while view:
|
|
105
|
+
written = os.write(fd, view)
|
|
106
|
+
if written <= 0:
|
|
107
|
+
raise OSError(errno.EIO, "short write")
|
|
108
|
+
view = view[written:]
|
|
109
|
+
|
|
110
|
+
def link_unsupported(exc):
|
|
111
|
+
unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
|
|
112
|
+
return getattr(exc, "errno", None) in unsupported
|
|
113
|
+
|
|
114
|
+
def link_no_replace(name, new_name, source_fd, target_fd):
|
|
115
|
+
linked = False
|
|
116
|
+
try:
|
|
117
|
+
os.link(name, new_name, src_dir_fd=source_fd, dst_dir_fd=target_fd, follow_symlinks=False)
|
|
118
|
+
linked = True
|
|
119
|
+
os.unlink(name, dir_fd=source_fd)
|
|
120
|
+
except Exception:
|
|
121
|
+
if linked:
|
|
122
|
+
try: os.unlink(new_name, dir_fd=target_fd)
|
|
123
|
+
except FileNotFoundError: pass
|
|
124
|
+
raise
|
|
125
|
+
os.fsync(source_fd)
|
|
126
|
+
if source_fd != target_fd:
|
|
127
|
+
os.fsync(target_fd)
|
|
128
|
+
|
|
129
|
+
def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basename, mode, expected=None, unlink_source=False):
|
|
130
|
+
source_fd = os.open(source_name, READ_FLAGS, dir_fd=source_parent_fd)
|
|
131
|
+
dest_fd = None
|
|
132
|
+
success = False
|
|
133
|
+
try:
|
|
134
|
+
if expected is not None:
|
|
135
|
+
source_stat = os.fstat(source_fd)
|
|
136
|
+
if source_stat.st_dev != expected.st_dev or source_stat.st_ino != expected.st_ino:
|
|
137
|
+
raise RuntimeError("fs-safe-source-mismatch")
|
|
138
|
+
dest_fd = os.open(basename, WRITE_FLAGS, mode, dir_fd=target_parent_fd)
|
|
139
|
+
copy_bytes(source_fd, dest_fd)
|
|
140
|
+
os.fsync(dest_fd)
|
|
141
|
+
success = True
|
|
142
|
+
finally:
|
|
143
|
+
os.close(source_fd)
|
|
144
|
+
if dest_fd is not None:
|
|
145
|
+
os.close(dest_fd)
|
|
146
|
+
if dest_fd is not None and not success:
|
|
147
|
+
try: os.unlink(basename, dir_fd=target_parent_fd)
|
|
148
|
+
except FileNotFoundError: pass
|
|
149
|
+
if unlink_source:
|
|
150
|
+
try:
|
|
151
|
+
os.unlink(source_name, dir_fd=source_parent_fd)
|
|
152
|
+
except Exception:
|
|
153
|
+
try: os.unlink(basename, dir_fd=target_parent_fd)
|
|
154
|
+
except FileNotFoundError: pass
|
|
155
|
+
raise
|
|
156
|
+
|
|
157
|
+
def commit_temp_file(parent_fd, temp_name, basename, overwrite, mode):
|
|
158
|
+
if overwrite:
|
|
159
|
+
os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
|
|
160
|
+
else:
|
|
161
|
+
try:
|
|
162
|
+
os.link(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd, follow_symlinks=False)
|
|
163
|
+
os.unlink(temp_name, dir_fd=parent_fd)
|
|
164
|
+
except OSError as exc:
|
|
165
|
+
if not link_unsupported(exc):
|
|
166
|
+
raise
|
|
167
|
+
copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode, unlink_source=True)
|
|
168
|
+
|
|
90
169
|
def stat_path(root_fd, payload):
|
|
91
170
|
relative = payload.get("relativePath", "")
|
|
92
171
|
segments = split_relative(relative)
|
|
@@ -161,7 +240,18 @@ def rename_path(root_fd, payload):
|
|
|
161
240
|
try:
|
|
162
241
|
from_stat = os.lstat(from_base, dir_fd=from_parent_fd)
|
|
163
242
|
reject_unsafe_endpoint(from_stat)
|
|
164
|
-
|
|
243
|
+
overwrite = payload.get("overwrite", True)
|
|
244
|
+
if not overwrite and stat.S_ISREG(from_stat.st_mode):
|
|
245
|
+
try:
|
|
246
|
+
link_no_replace(from_base, to_base, from_parent_fd, to_parent_fd)
|
|
247
|
+
except OSError as exc:
|
|
248
|
+
if not link_unsupported(exc):
|
|
249
|
+
raise
|
|
250
|
+
copy_file_no_replace(from_parent_fd, from_base, to_parent_fd, to_base, stat.S_IMODE(from_stat.st_mode), from_stat, True)
|
|
251
|
+
return None
|
|
252
|
+
if not overwrite and stat.S_ISDIR(from_stat.st_mode):
|
|
253
|
+
raise RuntimeError("fs-safe-directory-noreplace-unsupported")
|
|
254
|
+
if not overwrite:
|
|
165
255
|
try:
|
|
166
256
|
os.lstat(to_base, dir_fd=to_parent_fd)
|
|
167
257
|
raise FileExistsError(errno.EEXIST, "destination exists", to_base)
|
|
@@ -206,16 +296,11 @@ def write_path(root_fd, payload):
|
|
|
206
296
|
except FileNotFoundError:
|
|
207
297
|
pass
|
|
208
298
|
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
|
209
|
-
|
|
210
|
-
while view:
|
|
211
|
-
written = os.write(temp_fd, view)
|
|
212
|
-
if written <= 0:
|
|
213
|
-
raise OSError(errno.EIO, "short write")
|
|
214
|
-
view = view[written:]
|
|
299
|
+
write_all(temp_fd, data)
|
|
215
300
|
os.fsync(temp_fd)
|
|
216
301
|
os.close(temp_fd)
|
|
217
302
|
temp_fd = None
|
|
218
|
-
|
|
303
|
+
commit_temp_file(parent_fd, temp_name, basename, overwrite, mode)
|
|
219
304
|
temp_name = None
|
|
220
305
|
os.fsync(parent_fd)
|
|
221
306
|
result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
|
|
@@ -248,12 +333,6 @@ def copy_path(root_fd, payload):
|
|
|
248
333
|
if max_bytes >= 0 and source_stat.st_size > max_bytes:
|
|
249
334
|
raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
|
|
250
335
|
parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
|
|
251
|
-
if not overwrite:
|
|
252
|
-
try:
|
|
253
|
-
os.lstat(basename, dir_fd=parent_fd)
|
|
254
|
-
raise FileExistsError(errno.EEXIST, "destination exists", basename)
|
|
255
|
-
except FileNotFoundError:
|
|
256
|
-
pass
|
|
257
336
|
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
|
258
337
|
written_bytes = 0
|
|
259
338
|
while True:
|
|
@@ -272,7 +351,7 @@ def copy_path(root_fd, payload):
|
|
|
272
351
|
os.fsync(temp_fd)
|
|
273
352
|
os.close(temp_fd)
|
|
274
353
|
temp_fd = None
|
|
275
|
-
|
|
354
|
+
commit_temp_file(parent_fd, temp_name, basename, overwrite, mode)
|
|
276
355
|
temp_name = None
|
|
277
356
|
os.fsync(parent_fd)
|
|
278
357
|
result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
|
|
@@ -396,6 +475,9 @@ function mapWorkerError(response) {
|
|
|
396
475
|
if (message.includes("fs-safe-source-mismatch")) {
|
|
397
476
|
return new FsSafeError("path-mismatch", "source path changed during copy");
|
|
398
477
|
}
|
|
478
|
+
if (message.includes("fs-safe-directory-noreplace-unsupported")) {
|
|
479
|
+
return new FsSafeError("invalid-path", "directory moves require overwrite: true");
|
|
480
|
+
}
|
|
399
481
|
if (code === "FileNotFoundError" || errno === 2) {
|
|
400
482
|
return new FsSafeError("not-found", "file not found");
|
|
401
483
|
}
|
|
@@ -413,19 +495,19 @@ function mapWorkerError(response) {
|
|
|
413
495
|
}
|
|
414
496
|
return new FsSafeError("helper-failed", message);
|
|
415
497
|
}
|
|
416
|
-
function rejectPending(error) {
|
|
417
|
-
if (!worker) {
|
|
498
|
+
function rejectPending(error, targetWorker = worker) {
|
|
499
|
+
if (!targetWorker || worker !== targetWorker) {
|
|
418
500
|
return;
|
|
419
501
|
}
|
|
420
|
-
setWorkerRef(
|
|
421
|
-
for (const pending of
|
|
502
|
+
setWorkerRef(targetWorker, false);
|
|
503
|
+
for (const pending of targetWorker.pending.values()) {
|
|
422
504
|
pending.reject(error);
|
|
423
505
|
}
|
|
424
|
-
|
|
506
|
+
targetWorker.pending.clear();
|
|
425
507
|
worker = null;
|
|
426
508
|
}
|
|
427
|
-
function handleWorkerLine(line) {
|
|
428
|
-
if (
|
|
509
|
+
function handleWorkerLine(currentWorker, line) {
|
|
510
|
+
if (worker !== currentWorker || !line.trim()) {
|
|
429
511
|
return;
|
|
430
512
|
}
|
|
431
513
|
let decoded;
|
|
@@ -433,11 +515,11 @@ function handleWorkerLine(line) {
|
|
|
433
515
|
decoded = JSON.parse(line);
|
|
434
516
|
}
|
|
435
517
|
catch {
|
|
436
|
-
rejectPending(new FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`));
|
|
518
|
+
rejectPending(new FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`), currentWorker);
|
|
437
519
|
return;
|
|
438
520
|
}
|
|
439
521
|
if (typeof decoded !== "object" || decoded === null || !("id" in decoded)) {
|
|
440
|
-
rejectPending(new FsSafeError("helper-failed", "pinned helper returned invalid response"));
|
|
522
|
+
rejectPending(new FsSafeError("helper-failed", "pinned helper returned invalid response"), currentWorker);
|
|
441
523
|
return;
|
|
442
524
|
}
|
|
443
525
|
const response = decoded;
|
|
@@ -445,13 +527,13 @@ function handleWorkerLine(line) {
|
|
|
445
527
|
if (id === undefined) {
|
|
446
528
|
return;
|
|
447
529
|
}
|
|
448
|
-
const pending =
|
|
530
|
+
const pending = currentWorker.pending.get(id);
|
|
449
531
|
if (!pending) {
|
|
450
532
|
return;
|
|
451
533
|
}
|
|
452
|
-
|
|
453
|
-
if (
|
|
454
|
-
setWorkerRef(
|
|
534
|
+
currentWorker.pending.delete(id);
|
|
535
|
+
if (currentWorker.pending.size === 0) {
|
|
536
|
+
setWorkerRef(currentWorker, false);
|
|
455
537
|
}
|
|
456
538
|
if (response.ok === true) {
|
|
457
539
|
pending.resolve(response.result);
|
|
@@ -467,28 +549,28 @@ function getWorker() {
|
|
|
467
549
|
const child = spawn(resolvePython(), ["-u", "-c", PINNED_PYTHON_WORKER_SOURCE], {
|
|
468
550
|
stdio: ["pipe", "pipe", "pipe"],
|
|
469
551
|
});
|
|
470
|
-
|
|
552
|
+
const currentWorker = { child, pending: new Map(), stderr: "", stdoutBuffer: "" };
|
|
553
|
+
worker = currentWorker;
|
|
471
554
|
child.stdout.setEncoding("utf8");
|
|
472
555
|
child.stderr.setEncoding("utf8");
|
|
473
556
|
child.stdout.on("data", (chunk) => {
|
|
474
|
-
|
|
475
|
-
if (!current) {
|
|
557
|
+
if (worker !== currentWorker) {
|
|
476
558
|
return;
|
|
477
559
|
}
|
|
478
|
-
|
|
560
|
+
currentWorker.stdoutBuffer += chunk;
|
|
479
561
|
for (;;) {
|
|
480
|
-
const newline =
|
|
562
|
+
const newline = currentWorker.stdoutBuffer.indexOf("\n");
|
|
481
563
|
if (newline < 0) {
|
|
482
564
|
break;
|
|
483
565
|
}
|
|
484
|
-
const line =
|
|
485
|
-
|
|
486
|
-
handleWorkerLine(line);
|
|
566
|
+
const line = currentWorker.stdoutBuffer.slice(0, newline);
|
|
567
|
+
currentWorker.stdoutBuffer = currentWorker.stdoutBuffer.slice(newline + 1);
|
|
568
|
+
handleWorkerLine(currentWorker, line);
|
|
487
569
|
}
|
|
488
570
|
});
|
|
489
571
|
child.stderr.on("data", (chunk) => {
|
|
490
|
-
if (worker) {
|
|
491
|
-
|
|
572
|
+
if (worker === currentWorker) {
|
|
573
|
+
currentWorker.stderr = `${currentWorker.stderr}${chunk}`.slice(-4096);
|
|
492
574
|
}
|
|
493
575
|
});
|
|
494
576
|
child.once("error", (error) => {
|
|
@@ -497,17 +579,17 @@ function getWorker() {
|
|
|
497
579
|
: error instanceof Error
|
|
498
580
|
? error
|
|
499
581
|
: new Error(String(error));
|
|
500
|
-
rejectPending(mapped);
|
|
582
|
+
rejectPending(mapped, currentWorker);
|
|
501
583
|
});
|
|
502
584
|
child.once("close", (code, signal) => {
|
|
503
|
-
const stderr =
|
|
504
|
-
rejectPending(new FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`));
|
|
585
|
+
const stderr = currentWorker.stderr.trim();
|
|
586
|
+
rejectPending(new FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`), currentWorker);
|
|
505
587
|
});
|
|
506
588
|
process.once("exit", () => {
|
|
507
589
|
child.kill("SIGTERM");
|
|
508
590
|
});
|
|
509
|
-
setWorkerRef(
|
|
510
|
-
return
|
|
591
|
+
setWorkerRef(currentWorker, false);
|
|
592
|
+
return currentWorker;
|
|
511
593
|
}
|
|
512
594
|
function setRefable(value, ref) {
|
|
513
595
|
if (!value) {
|
package/dist/root-impl.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-impl.d.ts","sourceRoot":"","sources":["../src/root-impl.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"root-impl.d.ts","sourceRoot":"","sources":["../src/root-impl.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AA2CnD,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAIrD,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,YAAY,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,oBAAoB,CAAC;AAC5D,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,OAAO,CAAC;AAChD,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE/D,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,YAAY,EACZ,WAAW,GAAG,UAAU,GAAG,iBAAiB,GAAG,UAAU,CAC1D,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;AAEhE,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,MAAM,CAAC,GAAG;IACpE,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,MAAM,CAAC,GAAG;IAC3E,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,YAAY,EAAE,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC,GAAG;IAChF,eAAe,CAAC,EAAE,cAAc,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,gBAAgB,GAAG;IACpD,QAAQ,CAAC,EAAE,UAAU,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,KAAK,CAAC,EAAE,UAAU,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7C,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;AACpE,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAC;AAE5E,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IACjD,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC,CAAC;AAgCF,eAAO,MAAM,sBAAsB,QAAmB,CAAC;AA0HvD,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAEhC,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/C,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3E,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3E,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5E,QAAQ,CACN,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,eAAe,GAAG;QAAE,QAAQ,CAAC,EAAE,cAAc,CAAA;KAAE,GACxD,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,QAAQ,CAAC,CAAC,GAAG,OAAO,EAClB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,eAAe,GAAG;QAAE,QAAQ,CAAC,EAAE,cAAc,CAAA;KAAE,GACxD,OAAO,CAAC,CAAC,CAAC,CAAC;IACd,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/E,MAAM,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzE,YAAY,CACV,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC/B,MAAM,CACJ,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GAAG,MAAM,EACrB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,KAAK,CACH,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GAAG,MAAM,EACrB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,MAAM,CACJ,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GAAG,MAAM,EACrB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,SAAS,CACP,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,UAAU,CACR,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,MAAM,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3F,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,KAAK,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACnF,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,aAAa,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAClF,IAAI,CACF,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAChC,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AA6QD,wBAAsB,IAAI,CACxB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,YAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAEf;AAiFD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,UAAU,CAAC,CAOtB;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAG3F;AA0BD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,UAAU,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAqDF,wBAAsB,kCAAkC,CACtD,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAoCjB"}
|
package/dist/root-impl.js
CHANGED
|
@@ -20,6 +20,7 @@ import { resolveRootPath } from "./root-path.js";
|
|
|
20
20
|
import { assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, } from "./root-context.js";
|
|
21
21
|
import { isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, } from "./root-errors.js";
|
|
22
22
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
23
|
+
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
23
24
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
24
25
|
import { serializePathWrite } from "./write-queue.js";
|
|
25
26
|
function logWarn(message) {
|
|
@@ -260,12 +261,12 @@ class RootHandle {
|
|
|
260
261
|
}
|
|
261
262
|
async writeJson(relativePath, data, options = {}) {
|
|
262
263
|
const { replacer, space, trailingNewline = true, ...writeOptions } = options;
|
|
263
|
-
const json =
|
|
264
|
+
const json = stringifyJsonDocument(data, replacer, space);
|
|
264
265
|
await this.write(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
|
|
265
266
|
}
|
|
266
267
|
async createJson(relativePath, data, options = {}) {
|
|
267
268
|
const { replacer, space, trailingNewline = true, ...writeOptions } = options;
|
|
268
|
-
const json =
|
|
269
|
+
const json = stringifyJsonDocument(data, replacer, space);
|
|
269
270
|
await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
|
|
270
271
|
}
|
|
271
272
|
async copyIn(relativePath, sourcePath, options = {}) {
|
|
@@ -1050,6 +1051,9 @@ async function movePathFallback(root, params) {
|
|
|
1050
1051
|
if (sourceStat.isFile() && sourceStat.nlink > 1) {
|
|
1051
1052
|
throw new FsSafeError("hardlink", "hardlinked path not allowed");
|
|
1052
1053
|
}
|
|
1054
|
+
if (!params.overwrite && sourceStat.isDirectory()) {
|
|
1055
|
+
throw new FsSafeError("invalid-path", "directory moves require overwrite: true");
|
|
1056
|
+
}
|
|
1053
1057
|
if (!params.overwrite) {
|
|
1054
1058
|
try {
|
|
1055
1059
|
await fs.lstat(target.resolved);
|
package/dist/root-paths.js
CHANGED
|
@@ -62,7 +62,7 @@ export function resolvePathWithinRoot(params) {
|
|
|
62
62
|
}
|
|
63
63
|
const resolved = path.resolve(root, raw);
|
|
64
64
|
const rel = path.relative(root, resolved);
|
|
65
|
-
if (!rel || rel.startsWith(
|
|
65
|
+
if (!rel || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
66
66
|
return { ok: false, error: `Invalid path: must stay within ${params.scopeLabel}` };
|
|
67
67
|
}
|
|
68
68
|
return { ok: true, path: resolved };
|
|
@@ -118,7 +118,7 @@ async function resolveNearestExistingPath(targetPath) {
|
|
|
118
118
|
}
|
|
119
119
|
async function assertNoSymlinkSegments(params) {
|
|
120
120
|
const relative = path.relative(params.rootDir, params.targetPath);
|
|
121
|
-
if (relative.startsWith(
|
|
121
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
122
122
|
throw new Error(`Invalid path: must stay within ${params.scopeLabel}`);
|
|
123
123
|
}
|
|
124
124
|
let current = params.rootDir;
|
|
@@ -266,7 +266,10 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
266
266
|
const rootDir = path.resolve(params.rootDir);
|
|
267
267
|
const rootRealPath = await resolveRealPathIfExists(rootDir);
|
|
268
268
|
const root = rootRealPath ? await openRoot(rootDir) : undefined;
|
|
269
|
-
const isInRoot = (relativePath) => Boolean(relativePath) &&
|
|
269
|
+
const isInRoot = (relativePath) => Boolean(relativePath) &&
|
|
270
|
+
relativePath !== ".." &&
|
|
271
|
+
!relativePath.startsWith(`..${path.sep}`) &&
|
|
272
|
+
!path.isAbsolute(relativePath);
|
|
270
273
|
const resolveExistingRelativePath = async (requestedPath) => {
|
|
271
274
|
const raw = requestedPath.trim();
|
|
272
275
|
const lexicalPathResult = resolvePathWithinRoot({
|
|
@@ -316,6 +319,25 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
316
319
|
}
|
|
317
320
|
catch (err) {
|
|
318
321
|
if (allowMissingFallback && err instanceof FsSafeError && err.code === "not-found") {
|
|
322
|
+
if (!rootRealPath) {
|
|
323
|
+
resolvedPaths.push(pathResult.fallbackPath);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
await assertNoSymlinkSegments({
|
|
328
|
+
rootDir,
|
|
329
|
+
targetPath: pathResult.fallbackPath,
|
|
330
|
+
scopeLabel: params.scopeLabel,
|
|
331
|
+
});
|
|
332
|
+
const existingPath = await resolveNearestExistingPath(pathResult.fallbackPath);
|
|
333
|
+
const existingRealPath = await fs.realpath(existingPath);
|
|
334
|
+
if (!isPathInside(rootRealPath, existingRealPath)) {
|
|
335
|
+
return invalidPath(params.scopeLabel);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return invalidPath(params.scopeLabel);
|
|
340
|
+
}
|
|
319
341
|
resolvedPaths.push(pathResult.fallbackPath);
|
|
320
342
|
continue;
|
|
321
343
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAUrC,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAK1B,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,WAAW,CAAC,EAAE,2BAA2B,CAAC;IAC1C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,EAAE,CAAC,EAAE,mBAAmB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,CAAC;AAE7D,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;
|
|
1
|
+
{"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAUrC,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAK1B,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,WAAW,CAAC,EAAE,2BAA2B,CAAC;IAC1C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,EAAE,CAAC,EAAE,mBAAmB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,CAAC;AAE7D,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAsKF,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAa/B"}
|
package/dist/secure-file.js
CHANGED
|
@@ -9,7 +9,9 @@ import { inspectPathPermissions, isGroupReadable, isGroupWritable, isWorldReadab
|
|
|
9
9
|
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
|
|
10
10
|
const OPEN_READ_FLAGS = fsConstants.O_RDONLY | (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
|
|
11
11
|
function isAbsolutePathname(value) {
|
|
12
|
-
return path.isAbsolute(value) ||
|
|
12
|
+
return (path.isAbsolute(value) ||
|
|
13
|
+
(process.platform === "win32" &&
|
|
14
|
+
(isWindowsDriveLetterPath(value, "win32") || isWindowsNetworkPath(value, "win32"))));
|
|
13
15
|
}
|
|
14
16
|
function label(options) {
|
|
15
17
|
return options.label ?? "Secure file";
|
package/dist/sidecar-lock.d.ts
CHANGED
|
@@ -5,7 +5,13 @@ export type SidecarLockRetryOptions = {
|
|
|
5
5
|
maxTimeout?: number;
|
|
6
6
|
randomize?: boolean;
|
|
7
7
|
};
|
|
8
|
-
export type SidecarLockStaleRecovery = "fail-closed";
|
|
8
|
+
export type SidecarLockStaleRecovery = "fail-closed" | "remove-if-unchanged";
|
|
9
|
+
export type SidecarLockStaleSnapshot = {
|
|
10
|
+
lockPath: string;
|
|
11
|
+
normalizedTargetPath: string;
|
|
12
|
+
raw: string;
|
|
13
|
+
payload: Record<string, unknown> | null;
|
|
14
|
+
};
|
|
9
15
|
export type SidecarLockAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
10
16
|
targetPath: string;
|
|
11
17
|
lockPath?: string;
|
|
@@ -23,6 +29,7 @@ export type SidecarLockAcquireOptions<TPayload extends Record<string, unknown>>
|
|
|
23
29
|
nowMs: number;
|
|
24
30
|
heldByThisProcess: boolean;
|
|
25
31
|
}) => boolean | Promise<boolean>;
|
|
32
|
+
shouldRemoveStaleLock?: (snapshot: SidecarLockStaleSnapshot) => boolean | Promise<boolean>;
|
|
26
33
|
metadata?: Record<string, unknown>;
|
|
27
34
|
};
|
|
28
35
|
export type SidecarLockHandle = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"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"}
|
package/dist/sidecar-lock.js
CHANGED
|
@@ -64,6 +64,33 @@ async function lockSnapshotStillPresent(lockPath, observed) {
|
|
|
64
64
|
const current = await readLockSnapshot(lockPath);
|
|
65
65
|
return !!current && !!observed && snapshotMatches(current, observed);
|
|
66
66
|
}
|
|
67
|
+
async function removeStaleLockIfAllowed(params) {
|
|
68
|
+
if (!params.shouldRemoveStaleLock) {
|
|
69
|
+
return "not-approved";
|
|
70
|
+
}
|
|
71
|
+
if (params.snapshot.raw === undefined) {
|
|
72
|
+
return "not-approved";
|
|
73
|
+
}
|
|
74
|
+
if (!(await params.shouldRemoveStaleLock({
|
|
75
|
+
lockPath: params.lockPath,
|
|
76
|
+
normalizedTargetPath: params.normalizedTargetPath,
|
|
77
|
+
raw: params.snapshot.raw,
|
|
78
|
+
payload: params.snapshot.payload,
|
|
79
|
+
}))) {
|
|
80
|
+
return "not-approved";
|
|
81
|
+
}
|
|
82
|
+
const current = await readLockSnapshot(params.lockPath);
|
|
83
|
+
if (!current || !snapshotMatches(current, params.snapshot)) {
|
|
84
|
+
return "changed";
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await fs.rm(params.lockPath, { force: true });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return "not-approved";
|
|
91
|
+
}
|
|
92
|
+
return "removed";
|
|
93
|
+
}
|
|
67
94
|
function snapshotMatchesSync(lockPath, observed) {
|
|
68
95
|
try {
|
|
69
96
|
const stat = fsSync.lstatSync(lockPath);
|
|
@@ -248,17 +275,23 @@ export function createSidecarLockManager(key) {
|
|
|
248
275
|
if (!(await lockSnapshotStillPresent(lockPath, snapshot))) {
|
|
249
276
|
continue;
|
|
250
277
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// Fail closed and let callers choose a higher-level recovery path.
|
|
255
|
-
if ((options.staleRecovery ?? "fail-closed") === "fail-closed") {
|
|
256
|
-
throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
|
|
257
|
-
code: "file_lock_stale",
|
|
278
|
+
const staleRecovery = options.staleRecovery ?? "fail-closed";
|
|
279
|
+
if (staleRecovery === "remove-if-unchanged") {
|
|
280
|
+
const removal = await removeStaleLockIfAllowed({
|
|
258
281
|
lockPath,
|
|
259
282
|
normalizedTargetPath,
|
|
283
|
+
snapshot,
|
|
284
|
+
shouldRemoveStaleLock: options.shouldRemoveStaleLock,
|
|
260
285
|
});
|
|
286
|
+
if (removal === "removed" || removal === "changed") {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
261
289
|
}
|
|
290
|
+
throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
|
|
291
|
+
code: "file_lock_stale",
|
|
292
|
+
lockPath,
|
|
293
|
+
normalizedTargetPath,
|
|
294
|
+
});
|
|
262
295
|
}
|
|
263
296
|
const elapsed = Date.now() - startedAt;
|
|
264
297
|
if ((options.timeoutMs !== undefined &&
|
package/dist/timing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAAA,wBAAsB,WAAW,CAAC,CAAC,EACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,cAAc,GAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,KAAK,CAAA;CAErF,GACA,OAAO,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAAA,wBAAsB,WAAW,CAAC,CAAC,EACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,cAAc,GAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,KAAK,CAAA;CAErF,GACA,OAAO,CAAC,CAAC,CAAC,CA6BZ"}
|
package/dist/timing.js
CHANGED
|
@@ -12,7 +12,14 @@ export async function withTimeout(promise, timeoutMs, labelOrOptions = {
|
|
|
12
12
|
return await Promise.race([
|
|
13
13
|
promise,
|
|
14
14
|
new Promise((_, reject) => {
|
|
15
|
-
timeoutId = setTimeout(() =>
|
|
15
|
+
timeoutId = setTimeout(() => {
|
|
16
|
+
try {
|
|
17
|
+
reject(createError());
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
reject(error);
|
|
21
|
+
}
|
|
22
|
+
}, timeoutMs);
|
|
16
23
|
}),
|
|
17
24
|
]);
|
|
18
25
|
}
|
package/docs/config.md
CHANGED
|
@@ -56,7 +56,7 @@ Return the effective configuration: programmatic overrides win, then env vars, t
|
|
|
56
56
|
function configureFsSafeLocks(config: Partial<FsSafeLockConfig>): void;
|
|
57
57
|
|
|
58
58
|
type FsSafeLockConfig = {
|
|
59
|
-
staleRecovery: "fail-closed";
|
|
59
|
+
staleRecovery: "fail-closed" | "remove-if-unchanged";
|
|
60
60
|
staleMs?: number;
|
|
61
61
|
timeoutMs?: number;
|
|
62
62
|
retry?: FileLockRetryOptions;
|
|
@@ -65,7 +65,7 @@ type FsSafeLockConfig = {
|
|
|
65
65
|
|
|
66
66
|
Set process-wide defaults for sidecar lock options. This does **not** turn locking on globally; callers still need to pass `lock: true` or a lock options object for the specific JSON store/resource that needs cross-process coordination.
|
|
67
67
|
|
|
68
|
-
`staleRecovery`
|
|
68
|
+
`staleRecovery` defaults to `"fail-closed"`. `"remove-if-unchanged"` is available for callers that also pass `shouldRemoveStaleLock`; fs-safe re-reads the observed sidecar and removes it only when the raw content and file identity still match.
|
|
69
69
|
|
|
70
70
|
## `getFsSafeLockConfig()`
|
|
71
71
|
|
package/docs/index.md
CHANGED
|
@@ -64,7 +64,7 @@ await fs.remove("notes/archive/today.txt");
|
|
|
64
64
|
| [`extractArchive`](archive.md) | ZIP/TAR extraction with size, count, link, and traversal limits. |
|
|
65
65
|
| [Secret files](secret-file.md) | Mode-0600 credentials with size and TOCTOU defense. |
|
|
66
66
|
| [Permissions](permissions.md) | POSIX mode and Windows ACL inspection/remediation helpers. |
|
|
67
|
-
| [`acquireFileLock`](sidecar-lock.md) | Cross-process file lock with retry
|
|
67
|
+
| [`acquireFileLock`](sidecar-lock.md) | Cross-process file lock with retry, fail-closed stale-lock handling, and opt-in caller-approved stale removal. |
|
|
68
68
|
| [`FsSafeError`](errors.md) | Closed code union (with `policy` / `operational` category) you can branch on. |
|
|
69
69
|
| [`pathScope()`](path-scope.md) | Lower-level absolute-path boundary helper; lives behind `@openclaw/fs-safe/advanced`. |
|
|
70
70
|
| [`@openclaw/fs-safe/advanced`](advanced.md) | Directory of lower-level composition helpers (path scopes, regular-file I/O, install paths, sibling-temp writes, …). |
|
package/docs/json-store.md
CHANGED
|
@@ -53,7 +53,7 @@ type JsonStoreLockOptions = {
|
|
|
53
53
|
staleMs?: number; // default 30_000
|
|
54
54
|
timeoutMs?: number; // default 30_000
|
|
55
55
|
retry?: FileLockRetryOptions;
|
|
56
|
-
staleRecovery?: "fail-closed";
|
|
56
|
+
staleRecovery?: "fail-closed" | "remove-if-unchanged";
|
|
57
57
|
managerKey?: string; // default `fs-safe.json-store:<filePath>`
|
|
58
58
|
};
|
|
59
59
|
|
|
@@ -141,6 +141,8 @@ When `lock` is falsy, `read` / `write` / `update` are unlocked. The `update` sha
|
|
|
141
141
|
|
|
142
142
|
Process-wide lock defaults from `configureFsSafeLocks()` apply only after locking is explicitly enabled. They do not make JSON stores lock by default.
|
|
143
143
|
|
|
144
|
+
JSON store locks do not expose `shouldRemoveStaleLock`, so `staleRecovery: "remove-if-unchanged"` cannot remove a stale sidecar by itself. Use the lower-level [file lock](sidecar-lock.md) API when your application needs custom owner-liveness checks and caller-approved stale-lock removal.
|
|
145
|
+
|
|
144
146
|
The default `managerKey` namespaces the in-process `FileLockManager` per absolute file path, so two `jsonStore` calls on the same file share lock state automatically.
|
|
145
147
|
|
|
146
148
|
## Common patterns
|