@openclaw/fs-safe 0.4.7 → 0.5.1
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 +58 -0
- package/README.md +82 -43
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +5 -4
- package/dist/archive-errors.d.ts +11 -0
- package/dist/archive-errors.d.ts.map +1 -0
- package/dist/archive-errors.js +16 -0
- package/dist/archive-input.d.ts +12 -0
- package/dist/archive-input.d.ts.map +1 -0
- package/dist/archive-input.js +72 -0
- package/dist/archive-kind.d.ts +1 -1
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +18 -0
- package/dist/archive-limits.d.ts +10 -0
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +18 -0
- package/dist/archive-native.d.ts +18 -0
- package/dist/archive-native.d.ts.map +1 -0
- package/dist/archive-native.js +115 -0
- package/dist/archive-options.d.ts +21 -0
- package/dist/archive-options.d.ts.map +1 -0
- package/dist/archive-options.js +1 -0
- package/dist/archive-policy.d.ts +20 -0
- package/dist/archive-policy.d.ts.map +1 -0
- package/dist/archive-policy.js +29 -0
- package/dist/archive-read.d.ts +6 -0
- package/dist/archive-read.d.ts.map +1 -0
- package/dist/archive-read.js +200 -0
- package/dist/archive-staging.d.ts +2 -5
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +19 -10
- package/dist/archive-tar-meta.d.ts +6 -0
- package/dist/archive-tar-meta.d.ts.map +1 -0
- package/dist/archive-tar-meta.js +143 -0
- package/dist/archive-tar-runtime.d.ts +43 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -0
- package/dist/archive-tar-runtime.js +8 -0
- package/dist/archive-tar.d.ts +5 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +24 -6
- package/dist/archive-zip-entry.d.ts +15 -0
- package/dist/archive-zip-entry.d.ts.map +1 -0
- package/dist/archive-zip-entry.js +17 -0
- package/dist/archive.d.ts +7 -17
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +111 -108
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/containment.d.ts +2 -0
- package/dist/containment.d.ts.map +1 -0
- package/dist/containment.js +1 -0
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +24 -2
- package/dist/durability.d.ts +2 -0
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +2 -0
- package/dist/errors.d.ts +4 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/file-hash.d.ts +10 -0
- package/dist/file-hash.d.ts.map +1 -0
- package/dist/file-hash.js +73 -0
- package/dist/file-lock-sync.d.ts +35 -0
- package/dist/file-lock-sync.d.ts.map +1 -0
- package/dist/file-lock-sync.js +233 -0
- package/dist/file-lock.d.ts +2 -0
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +4 -1
- package/dist/guarded-mkdir.d.ts +7 -1
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +45 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +30 -15
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +101 -17
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +79 -0
- package/dist/native-binding.d.ts.map +1 -0
- package/dist/native-binding.js +1 -0
- package/dist/native-config.d.ts +17 -0
- package/dist/native-config.d.ts.map +1 -0
- package/dist/native-config.js +71 -0
- package/dist/native-operations.d.ts +21 -0
- package/dist/native-operations.d.ts.map +1 -0
- package/dist/native-operations.js +115 -0
- package/dist/native-pinned-write.d.ts +5 -0
- package/dist/native-pinned-write.d.ts.map +1 -0
- package/dist/native-pinned-write.js +115 -0
- package/dist/native.d.ts +14 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +204 -0
- package/dist/output-sibling.d.ts +8 -0
- package/dist/output-sibling.d.ts.map +1 -0
- package/dist/output-sibling.js +122 -0
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +32 -5
- package/dist/owner-dacl.d.ts +31 -0
- package/dist/owner-dacl.d.ts.map +1 -0
- package/dist/owner-dacl.js +27 -0
- package/dist/path-policy.js +1 -1
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +3 -2
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-public.js +2 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +6 -0
- package/dist/pinned-operation.d.ts +2 -0
- package/dist/pinned-operation.d.ts.map +1 -0
- package/dist/pinned-operation.js +35 -0
- package/dist/pinned-write.d.ts +3 -16
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +20 -85
- package/dist/private-directory.d.ts +5 -0
- package/dist/private-directory.d.ts.map +1 -0
- package/dist/private-directory.js +13 -0
- package/dist/private-temp-workspace.d.ts +6 -2
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +40 -13
- package/dist/publish-file-failure.d.ts +27 -0
- package/dist/publish-file-failure.d.ts.map +1 -0
- package/dist/publish-file-failure.js +21 -0
- package/dist/publish-file.d.ts +21 -0
- package/dist/publish-file.d.ts.map +1 -0
- package/dist/publish-file.js +351 -0
- package/dist/read-opened-file.d.ts +3 -0
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +1 -0
- package/dist/replace-file-copy-fallback.d.ts +34 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -0
- package/dist/replace-file-copy-fallback.js +339 -0
- package/dist/replace-file.d.ts +6 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +39 -90
- package/dist/root-context.d.ts +4 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +20 -1
- package/dist/root-impl.d.ts +5 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +128 -225
- package/dist/root-path-existing.d.ts +3 -0
- package/dist/root-path-existing.d.ts.map +1 -0
- package/dist/root-path-existing.js +69 -0
- package/dist/root-path.d.ts +1 -2
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +67 -81
- package/dist/root-walk.d.ts +41 -0
- package/dist/root-walk.d.ts.map +1 -0
- package/dist/root-walk.js +114 -0
- package/dist/root.d.ts +2 -0
- package/dist/root.d.ts.map +1 -1
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +16 -4
- package/dist/secret-file.d.ts +5 -2
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +20 -2
- package/dist/secret-read-async.d.ts +4 -0
- package/dist/secret-read-async.d.ts.map +1 -0
- package/dist/secret-read-async.js +95 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +1 -0
- package/dist/sidecar-lock-handle.d.ts +20 -0
- package/dist/sidecar-lock-handle.d.ts.map +1 -0
- package/dist/sidecar-lock-handle.js +28 -0
- package/dist/sidecar-lock-policy.d.ts +10 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -0
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts +22 -6
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +106 -20
- package/dist/sidecar-lock-types.d.ts +56 -0
- package/dist/sidecar-lock-types.d.ts.map +1 -0
- package/dist/sidecar-lock-types.js +1 -0
- package/dist/sidecar-lock.d.ts +2 -45
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +94 -55
- package/dist/temp-cleanup.d.ts +7 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +35 -5
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +51 -3
- package/dist/temp.d.ts +2 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +3 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/windows-permissions-native.d.ts +9 -0
- package/dist/windows-permissions-native.d.ts.map +1 -0
- package/dist/windows-permissions-native.js +31 -0
- package/docs/archive.md +131 -13
- package/docs/atomic.md +30 -1
- package/docs/config.md +51 -21
- package/docs/durability.md +211 -1
- package/docs/errors.md +36 -6
- package/docs/filename.md +0 -0
- package/docs/index.md +11 -8
- package/docs/install.md +27 -26
- package/docs/json-store.md +18 -3
- package/docs/migrating-to-0.5.md +196 -0
- package/docs/native-helper.md +85 -0
- package/docs/native.md +134 -0
- package/docs/output.md +37 -11
- package/docs/path.md +1 -1
- package/docs/permissions.md +84 -1
- package/docs/quickstart.md +5 -2
- package/docs/reading.md +4 -4
- package/docs/root.md +31 -15
- package/docs/secret-file.md +50 -1
- package/docs/security-model.md +29 -8
- package/docs/sidecar-lock.md +92 -7
- package/docs/temp.md +24 -1
- package/docs/test-hooks.md +22 -2
- package/docs/testing.md +18 -7
- package/docs/types.md +3 -1
- package/docs/walk.md +55 -0
- package/docs/writing.md +3 -3
- package/package.json +10 -4
- package/dist/pinned-helper.d.ts +0 -9
- package/dist/pinned-helper.d.ts.map +0 -1
- package/dist/pinned-helper.js +0 -19
- package/dist/pinned-path.d.ts +0 -7
- package/dist/pinned-path.d.ts.map +0 -1
- package/dist/pinned-path.js +0 -21
- package/dist/pinned-python-config.d.ts +0 -9
- package/dist/pinned-python-config.d.ts.map +0 -1
- package/dist/pinned-python-config.js +0 -38
- package/dist/pinned-python.d.ts +0 -12
- package/dist/pinned-python.d.ts.map +0 -1
- package/dist/pinned-python.js +0 -688
- package/docs/python-helper.md +0 -103
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { readFileHandleBounded } from "./bounded-read.js";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
6
|
+
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
7
|
+
import { DEFAULT_SECRET_FILE_MAX_BYTES, } from "./secret-file.js";
|
|
8
|
+
function pathErrorCode(error) {
|
|
9
|
+
const code = error.code;
|
|
10
|
+
return code === "ENOENT" || code === "ENOTDIR" ? "not-found" : "invalid-path";
|
|
11
|
+
}
|
|
12
|
+
async function readSecretFileOutcome(filePath, label, options) {
|
|
13
|
+
const resolvedPath = resolveHomeRelativePath(filePath.trim());
|
|
14
|
+
if (!resolvedPath) {
|
|
15
|
+
return { ok: false, code: "invalid-path", message: `${label} file path is empty.` };
|
|
16
|
+
}
|
|
17
|
+
const maxBytes = options.maxBytes ?? DEFAULT_SECRET_FILE_MAX_BYTES;
|
|
18
|
+
let previewStat;
|
|
19
|
+
try {
|
|
20
|
+
previewStat = await fs.lstat(resolvedPath);
|
|
21
|
+
if (previewStat.isSymbolicLink()) {
|
|
22
|
+
if (options.rejectSymlink) {
|
|
23
|
+
return { ok: false, code: "symlink", message: `${label} file at ${resolvedPath} must not be a symlink.` };
|
|
24
|
+
}
|
|
25
|
+
previewStat = await fs.stat(resolvedPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
code: pathErrorCode(error),
|
|
33
|
+
error: normalized,
|
|
34
|
+
message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (!previewStat.isFile()) {
|
|
38
|
+
return { ok: false, code: "not-file", message: `${label} file at ${resolvedPath} must be a regular file.` };
|
|
39
|
+
}
|
|
40
|
+
if (options.rejectHardlinks !== false && previewStat.nlink > 1) {
|
|
41
|
+
return { ok: false, code: "hardlink", message: `${label} file at ${resolvedPath} must not be hardlinked.` };
|
|
42
|
+
}
|
|
43
|
+
if (previewStat.size > maxBytes) {
|
|
44
|
+
return { ok: false, code: "too-large", message: `${label} file at ${resolvedPath} exceeds ${maxBytes} bytes.` };
|
|
45
|
+
}
|
|
46
|
+
let handle;
|
|
47
|
+
try {
|
|
48
|
+
const realPath = await fs.realpath(resolvedPath);
|
|
49
|
+
const noFollow = process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
|
|
50
|
+
? fsSync.constants.O_NOFOLLOW
|
|
51
|
+
: 0;
|
|
52
|
+
handle = await fs.open(realPath, fsSync.constants.O_RDONLY | noFollow);
|
|
53
|
+
const openedStat = await handle.stat();
|
|
54
|
+
const pathStat = await fs.lstat(realPath);
|
|
55
|
+
if (!openedStat.isFile() ||
|
|
56
|
+
!pathStat.isFile() ||
|
|
57
|
+
!sameFileIdentity(previewStat, openedStat) ||
|
|
58
|
+
!sameFileIdentity(pathStat, openedStat) ||
|
|
59
|
+
(options.rejectHardlinks !== false && openedStat.nlink > 1)) {
|
|
60
|
+
throw new FsSafeError("path-mismatch", "security validation failed");
|
|
61
|
+
}
|
|
62
|
+
const secret = (await readFileHandleBounded(handle, maxBytes)).toString("utf8").trim();
|
|
63
|
+
return secret
|
|
64
|
+
? { ok: true, secret }
|
|
65
|
+
: { ok: false, code: "invalid-path", message: `${label} file at ${resolvedPath} is empty.` };
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
code: error instanceof FsSafeError ? error.code : pathErrorCode(error) === "not-found" ? "not-found" : "path-mismatch",
|
|
72
|
+
error: normalized,
|
|
73
|
+
message: `Failed to read ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await handle?.close().catch(() => undefined);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export async function readSecretFile(filePath, label, options = {}) {
|
|
81
|
+
const result = await readSecretFileOutcome(filePath, label, options);
|
|
82
|
+
if (result.ok)
|
|
83
|
+
return result.secret;
|
|
84
|
+
throw new FsSafeError(result.code, result.message, { cause: result.error });
|
|
85
|
+
}
|
|
86
|
+
export async function tryReadSecretFile(filePath, label, options = {}) {
|
|
87
|
+
if (!filePath?.trim())
|
|
88
|
+
return undefined;
|
|
89
|
+
const result = await readSecretFileOutcome(filePath, label, options);
|
|
90
|
+
if (result.ok)
|
|
91
|
+
return result.secret;
|
|
92
|
+
if (result.code === "not-found")
|
|
93
|
+
return undefined;
|
|
94
|
+
throw new FsSafeError(result.code, result.message, { cause: result.error });
|
|
95
|
+
}
|
package/dist/secret.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { DEFAULT_SECRET_FILE_MAX_BYTES, PRIVATE_SECRET_DIR_MODE, PRIVATE_SECRET_FILE_MODE, readSecretFileSync, tryReadSecretFileSync, writeSecretFileAtomic, type SecretFileReadOptions, } from "./secret-file.js";
|
|
1
|
+
export { createSecretFileAtomic, DEFAULT_SECRET_FILE_MAX_BYTES, PRIVATE_SECRET_DIR_MODE, PRIVATE_SECRET_FILE_MODE, readSecretFileSync, tryReadSecretFileSync, writeSecretFileAtomic, type SecretFileReadOptions, } from "./secret-file.js";
|
|
2
|
+
export { readSecretFile, tryReadSecretFile } from "./secret-read-async.js";
|
|
2
3
|
//# sourceMappingURL=secret.d.ts.map
|
package/dist/secret.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret.d.ts","sourceRoot":"","sources":["../src/secret.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,6BAA6B,EAC7B,uBAAuB,EACvB,wBAAwB,EACxB,kBAAkB,EAClB,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"secret.d.ts","sourceRoot":"","sources":["../src/secret.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,6BAA6B,EAC7B,uBAAuB,EACvB,wBAAwB,EACxB,kBAAkB,EAClB,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/secret.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { DEFAULT_SECRET_FILE_MAX_BYTES, PRIVATE_SECRET_DIR_MODE, PRIVATE_SECRET_FILE_MODE, readSecretFileSync, tryReadSecretFileSync, writeSecretFileAtomic, } from "./secret-file.js";
|
|
1
|
+
export { createSecretFileAtomic, DEFAULT_SECRET_FILE_MAX_BYTES, PRIVATE_SECRET_DIR_MODE, PRIVATE_SECRET_FILE_MODE, readSecretFileSync, tryReadSecretFileSync, writeSecretFileAtomic, } from "./secret-file.js";
|
|
2
|
+
export { readSecretFile, tryReadSecretFile } from "./secret-read-async.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5C,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AA8BF,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5C,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AA8BF,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CA0CxC;AAkBD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8ChB"}
|
package/dist/sibling-temp.js
CHANGED
|
@@ -51,6 +51,7 @@ export async function writeSiblingTempFile(options) {
|
|
|
51
51
|
try {
|
|
52
52
|
tempExists = true;
|
|
53
53
|
const result = await options.writeTemp(tempPath);
|
|
54
|
+
unregisterTempPath.setIdentity(await fs.lstat(tempPath));
|
|
54
55
|
if (options.mode !== undefined) {
|
|
55
56
|
await fs.chmod(tempPath, options.mode).catch(() => undefined);
|
|
56
57
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Root } from "./root-impl.js";
|
|
2
|
+
import { type SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
|
|
3
|
+
import type { SidecarLockHandle } from "./sidecar-lock-types.js";
|
|
4
|
+
export declare function createSidecarLockHandle(params: {
|
|
5
|
+
lockPath: string;
|
|
6
|
+
normalizedTargetPath: string;
|
|
7
|
+
verifyStillHeld: () => Promise<boolean>;
|
|
8
|
+
release: () => Promise<unknown>;
|
|
9
|
+
}): SidecarLockHandle;
|
|
10
|
+
export declare function createHeldSidecarLockHandle(params: {
|
|
11
|
+
normalizedTargetPath: string;
|
|
12
|
+
held: {
|
|
13
|
+
lockPath: string;
|
|
14
|
+
snapshot: SidecarLockSnapshot;
|
|
15
|
+
lockRoot?: Root;
|
|
16
|
+
parsePayload?: (raw: string) => unknown;
|
|
17
|
+
};
|
|
18
|
+
release: () => Promise<unknown>;
|
|
19
|
+
}): SidecarLockHandle;
|
|
20
|
+
//# sourceMappingURL=sidecar-lock-handle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sidecar-lock-handle.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACjC,GAAG,iBAAiB,CAcpB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,oBAAoB,EAAE,MAAM,CAAC;IAC7B,IAAI,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,mBAAmB,CAAC;QAC9B,QAAQ,CAAC,EAAE,IAAI,CAAC;QAChB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;KACzC,CAAC;IACF,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACjC,GAAG,iBAAiB,CAWpB"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { sidecarLockSnapshotStillPresent, } from "./sidecar-lock-reclaim.js";
|
|
2
|
+
export function createSidecarLockHandle(params) {
|
|
3
|
+
let released = false;
|
|
4
|
+
const release = async () => {
|
|
5
|
+
if (released)
|
|
6
|
+
return;
|
|
7
|
+
released = true;
|
|
8
|
+
await params.release();
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
lockPath: params.lockPath,
|
|
12
|
+
normalizedTargetPath: params.normalizedTargetPath,
|
|
13
|
+
verifyStillHeld: params.verifyStillHeld,
|
|
14
|
+
release,
|
|
15
|
+
[Symbol.asyncDispose]: release,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function createHeldSidecarLockHandle(params) {
|
|
19
|
+
return createSidecarLockHandle({
|
|
20
|
+
lockPath: params.held.lockPath,
|
|
21
|
+
normalizedTargetPath: params.normalizedTargetPath,
|
|
22
|
+
verifyStillHeld: async () => await sidecarLockSnapshotStillPresent(params.held.lockPath, params.held.snapshot, {
|
|
23
|
+
lockRoot: params.held.lockRoot,
|
|
24
|
+
parsePayload: params.held.parsePayload,
|
|
25
|
+
}),
|
|
26
|
+
release: params.release,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SidecarLockRetryOptions } from "./sidecar-lock-types.js";
|
|
2
|
+
export declare function computeSidecarLockDelayMs(retry: SidecarLockRetryOptions, attempt: number): number;
|
|
3
|
+
export declare function sidecarLockPayloadIsStale(payload: unknown, staleMs: number, nowMs: number): boolean;
|
|
4
|
+
export declare function defaultSidecarLockShouldReclaim(params: {
|
|
5
|
+
lockPath: string;
|
|
6
|
+
payload: unknown;
|
|
7
|
+
staleMs: number;
|
|
8
|
+
nowMs: number;
|
|
9
|
+
}): Promise<boolean>;
|
|
10
|
+
//# sourceMappingURL=sidecar-lock-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAEvE,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAOjG;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAUT;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnB"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
export function computeSidecarLockDelayMs(retry, attempt) {
|
|
3
|
+
const minTimeout = retry.minTimeout ?? 50;
|
|
4
|
+
const maxTimeout = retry.maxTimeout ?? 1000;
|
|
5
|
+
const factor = retry.factor ?? 1;
|
|
6
|
+
const base = Math.min(maxTimeout, Math.max(minTimeout, minTimeout * factor ** attempt));
|
|
7
|
+
const jitter = retry.randomize ? 1 + Math.random() : 1;
|
|
8
|
+
return Math.min(maxTimeout, Math.round(base * jitter));
|
|
9
|
+
}
|
|
10
|
+
export function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
|
|
11
|
+
const createdAt = payload &&
|
|
12
|
+
typeof payload === "object" &&
|
|
13
|
+
"createdAt" in payload &&
|
|
14
|
+
typeof payload.createdAt === "string"
|
|
15
|
+
? payload.createdAt
|
|
16
|
+
: "";
|
|
17
|
+
const createdAtMs = Date.parse(createdAt);
|
|
18
|
+
return Number.isFinite(createdAtMs) && nowMs - createdAtMs > staleMs;
|
|
19
|
+
}
|
|
20
|
+
export async function defaultSidecarLockShouldReclaim(params) {
|
|
21
|
+
if (sidecarLockPayloadIsStale(params.payload, params.staleMs, params.nowMs))
|
|
22
|
+
return true;
|
|
23
|
+
try {
|
|
24
|
+
return params.nowMs - (await fs.stat(params.lockPath)).mtimeMs > params.staleMs;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Stats } from "node:fs";
|
|
2
|
+
import type { Root } from "./root-impl.js";
|
|
2
3
|
export type SidecarLockStaleSnapshot = {
|
|
3
4
|
lockPath: string;
|
|
4
5
|
normalizedTargetPath: string;
|
|
5
6
|
raw: string;
|
|
6
|
-
payload:
|
|
7
|
+
payload: unknown;
|
|
7
8
|
};
|
|
8
9
|
export type SidecarLockSnapshot = {
|
|
9
10
|
raw?: string;
|
|
10
|
-
payload:
|
|
11
|
+
payload: unknown;
|
|
11
12
|
stat?: Stats;
|
|
12
13
|
ownershipToken?: string;
|
|
13
14
|
};
|
|
@@ -16,10 +17,23 @@ export declare function serializeSidecarLockPayload(payload: Record<string, unkn
|
|
|
16
17
|
raw: string;
|
|
17
18
|
ownershipToken: string;
|
|
18
19
|
};
|
|
19
|
-
export declare function
|
|
20
|
+
export declare function relativeSidecarLockPath(lockRoot: Root, lockPath: string): string;
|
|
21
|
+
export declare function parseSidecarLockPayload(raw: string, parser?: (raw: string) => unknown): unknown;
|
|
22
|
+
export declare function readSidecarLockSnapshot(lockPath: string, options?: {
|
|
23
|
+
lockRoot?: Root;
|
|
24
|
+
parsePayload?: (raw: string) => unknown;
|
|
25
|
+
}): Promise<SidecarLockSnapshot | null>;
|
|
26
|
+
export declare function readSidecarLockSnapshotSync(lockPath: string, parsePayload?: (raw: string) => unknown): SidecarLockSnapshot | null;
|
|
27
|
+
export declare function removeSidecarLockIfUnchangedSync(lockPath: string, observed: SidecarLockSnapshot): boolean;
|
|
20
28
|
export declare function sidecarLockSnapshotMatches(current: SidecarLockSnapshot, observed: SidecarLockSnapshot): boolean;
|
|
21
|
-
export declare function removeSidecarLockIfUnchanged(lockPath: string, observed: SidecarLockSnapshot | null
|
|
22
|
-
|
|
29
|
+
export declare function removeSidecarLockIfUnchanged(lockPath: string, observed: SidecarLockSnapshot | null, options?: {
|
|
30
|
+
lockRoot?: Root;
|
|
31
|
+
parsePayload?: (raw: string) => unknown;
|
|
32
|
+
}): Promise<boolean>;
|
|
33
|
+
export declare function sidecarLockSnapshotStillPresent(lockPath: string, observed: SidecarLockSnapshot | null, options?: {
|
|
34
|
+
lockRoot?: Root;
|
|
35
|
+
parsePayload?: (raw: string) => unknown;
|
|
36
|
+
}): Promise<boolean>;
|
|
23
37
|
export declare function sidecarReclaimGuardExists(pathname: string): Promise<boolean>;
|
|
24
38
|
export declare function tryAcquireSidecarReclaimGuard(reclaimGuards: Set<string>, reclaimGuardPath: string): Promise<boolean>;
|
|
25
39
|
export declare function releaseSidecarReclaimGuard(reclaimGuards: Set<string>, reclaimGuardPath: string): Promise<void>;
|
|
@@ -28,5 +42,7 @@ export declare function removeStaleSidecarLockIfAllowed(params: {
|
|
|
28
42
|
normalizedTargetPath: string;
|
|
29
43
|
snapshot: SidecarLockSnapshot;
|
|
30
44
|
shouldRemoveStaleLock?: (snapshot: SidecarLockStaleSnapshot) => boolean | Promise<boolean>;
|
|
45
|
+
lockRoot?: Root;
|
|
46
|
+
parsePayload?: (raw: string) => unknown;
|
|
31
47
|
}): Promise<"removed" | "changed" | "not-approved">;
|
|
32
48
|
//# sourceMappingURL=sidecar-lock-reclaim.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"AACA,
|
|
1
|
+
{"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import fsSync, {} from "node:fs";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { readFileHandleBounded } from "./bounded-read.js";
|
|
6
|
+
import { FsSafeError } from "./errors.js";
|
|
3
7
|
import { sameFileIdentity } from "./file-identity.js";
|
|
8
|
+
const MAX_LOCK_PAYLOAD_BYTES = 1024 * 1024;
|
|
4
9
|
const SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES = 16;
|
|
5
10
|
const SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS = SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES * 8;
|
|
6
11
|
const SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX = "\t".repeat(8);
|
|
@@ -24,28 +29,98 @@ export function serializeSidecarLockPayload(payload) {
|
|
|
24
29
|
ownershipToken,
|
|
25
30
|
};
|
|
26
31
|
}
|
|
27
|
-
export
|
|
32
|
+
export function relativeSidecarLockPath(lockRoot, lockPath) {
|
|
33
|
+
const resolved = path.resolve(lockPath);
|
|
34
|
+
const lexicalRelative = path.relative(lockRoot.rootDir, resolved);
|
|
35
|
+
const relative = lexicalRelative !== ".." &&
|
|
36
|
+
!lexicalRelative.startsWith(`..${path.sep}`) &&
|
|
37
|
+
!path.isAbsolute(lexicalRelative)
|
|
38
|
+
? lexicalRelative
|
|
39
|
+
: path.relative(lockRoot.rootReal, resolved);
|
|
40
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
41
|
+
throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
|
|
42
|
+
}
|
|
43
|
+
return relative.split(path.sep).join(path.posix.sep);
|
|
44
|
+
}
|
|
45
|
+
export function parseSidecarLockPayload(raw, parser) {
|
|
46
|
+
if (parser) {
|
|
47
|
+
return parser(raw);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function readSidecarLockSnapshot(lockPath, options = {}) {
|
|
28
58
|
try {
|
|
59
|
+
if (options.lockRoot) {
|
|
60
|
+
const opened = await options.lockRoot.open(relativeSidecarLockPath(options.lockRoot, lockPath));
|
|
61
|
+
try {
|
|
62
|
+
const raw = (await readFileHandleBounded(opened.handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
|
|
63
|
+
return {
|
|
64
|
+
raw,
|
|
65
|
+
payload: parseSidecarLockPayload(raw, options.parsePayload),
|
|
66
|
+
stat: opened.stat,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
await opened.handle.close().catch(() => undefined);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
29
73
|
const stat = await fs.lstat(lockPath);
|
|
30
74
|
const raw = await fs.readFile(lockPath, "utf8");
|
|
31
|
-
|
|
32
|
-
const parsed = JSON.parse(raw);
|
|
33
|
-
const payload = parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
34
|
-
? parsed
|
|
35
|
-
: null;
|
|
36
|
-
return { raw, payload, stat };
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return { raw, payload: null, stat };
|
|
40
|
-
}
|
|
75
|
+
return { raw, payload: parseSidecarLockPayload(raw, options.parsePayload), stat };
|
|
41
76
|
}
|
|
42
77
|
catch (err) {
|
|
43
|
-
if (err.code === "ENOENT"
|
|
78
|
+
if (err.code === "ENOENT" ||
|
|
79
|
+
(err instanceof FsSafeError && err.code === "not-found")) {
|
|
44
80
|
return null;
|
|
45
81
|
}
|
|
46
82
|
throw err;
|
|
47
83
|
}
|
|
48
84
|
}
|
|
85
|
+
export function readSidecarLockSnapshotSync(lockPath, parsePayload) {
|
|
86
|
+
let fd;
|
|
87
|
+
try {
|
|
88
|
+
const before = fsSync.lstatSync(lockPath);
|
|
89
|
+
if (!before.isFile() || before.isSymbolicLink())
|
|
90
|
+
return null;
|
|
91
|
+
const noFollow = process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
|
|
92
|
+
? fsSync.constants.O_NOFOLLOW
|
|
93
|
+
: 0;
|
|
94
|
+
fd = fsSync.openSync(lockPath, fsSync.constants.O_RDONLY | noFollow);
|
|
95
|
+
const opened = fsSync.fstatSync(fd);
|
|
96
|
+
const raw = fsSync.readFileSync(fd, "utf8");
|
|
97
|
+
const after = fsSync.lstatSync(lockPath);
|
|
98
|
+
if (!sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
|
|
99
|
+
return null;
|
|
100
|
+
return {
|
|
101
|
+
raw,
|
|
102
|
+
payload: parseSidecarLockPayload(raw, parsePayload),
|
|
103
|
+
stat: after,
|
|
104
|
+
ownershipToken: readSidecarLockOwnershipToken(raw),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error.code === "ENOENT")
|
|
109
|
+
return null;
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
if (fd !== undefined)
|
|
114
|
+
fsSync.closeSync(fd);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function removeSidecarLockIfUnchangedSync(lockPath, observed) {
|
|
118
|
+
const current = readSidecarLockSnapshotSync(lockPath);
|
|
119
|
+
if (!current || !sidecarLockSnapshotMatches(current, observed))
|
|
120
|
+
return false;
|
|
121
|
+
fsSync.rmSync(lockPath);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
49
124
|
export function sidecarLockSnapshotMatches(current, observed) {
|
|
50
125
|
if (observed.ownershipToken !== undefined) {
|
|
51
126
|
return (current.stat?.isFile() === true &&
|
|
@@ -63,16 +138,21 @@ export function sidecarLockSnapshotMatches(current, observed) {
|
|
|
63
138
|
}
|
|
64
139
|
return observed.stat !== undefined && current.stat !== undefined;
|
|
65
140
|
}
|
|
66
|
-
export async function removeSidecarLockIfUnchanged(lockPath, observed) {
|
|
67
|
-
const current = await readSidecarLockSnapshot(lockPath);
|
|
141
|
+
export async function removeSidecarLockIfUnchanged(lockPath, observed, options = {}) {
|
|
142
|
+
const current = await readSidecarLockSnapshot(lockPath, options);
|
|
68
143
|
if (!current || !observed || !sidecarLockSnapshotMatches(current, observed)) {
|
|
69
144
|
return false;
|
|
70
145
|
}
|
|
71
|
-
|
|
146
|
+
if (options.lockRoot) {
|
|
147
|
+
await options.lockRoot.remove(relativeSidecarLockPath(options.lockRoot, lockPath)).catch(() => undefined);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
await fs.rm(lockPath, { force: true }).catch(() => undefined);
|
|
151
|
+
}
|
|
72
152
|
return true;
|
|
73
153
|
}
|
|
74
|
-
export async function sidecarLockSnapshotStillPresent(lockPath, observed) {
|
|
75
|
-
const current = await readSidecarLockSnapshot(lockPath);
|
|
154
|
+
export async function sidecarLockSnapshotStillPresent(lockPath, observed, options = {}) {
|
|
155
|
+
const current = await readSidecarLockSnapshot(lockPath, options);
|
|
76
156
|
return !!current && !!observed && sidecarLockSnapshotMatches(current, observed);
|
|
77
157
|
}
|
|
78
158
|
export async function sidecarReclaimGuardExists(pathname) {
|
|
@@ -108,7 +188,8 @@ export async function removeStaleSidecarLockIfAllowed(params) {
|
|
|
108
188
|
if (!params.shouldRemoveStaleLock || params.snapshot.raw === undefined) {
|
|
109
189
|
return "not-approved";
|
|
110
190
|
}
|
|
111
|
-
|
|
191
|
+
const ioOptions = { lockRoot: params.lockRoot, parsePayload: params.parsePayload };
|
|
192
|
+
if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot, ioOptions))) {
|
|
112
193
|
return "changed";
|
|
113
194
|
}
|
|
114
195
|
if (!(await params.shouldRemoveStaleLock({
|
|
@@ -119,11 +200,16 @@ export async function removeStaleSidecarLockIfAllowed(params) {
|
|
|
119
200
|
}))) {
|
|
120
201
|
return "not-approved";
|
|
121
202
|
}
|
|
122
|
-
if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot))) {
|
|
203
|
+
if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot, ioOptions))) {
|
|
123
204
|
return "changed";
|
|
124
205
|
}
|
|
125
206
|
try {
|
|
126
|
-
|
|
207
|
+
if (params.lockRoot) {
|
|
208
|
+
await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath));
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
await fs.rm(params.lockPath);
|
|
212
|
+
}
|
|
127
213
|
return "removed";
|
|
128
214
|
}
|
|
129
215
|
catch (err) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Root } from "./root-impl.js";
|
|
2
|
+
import type { SidecarLockStaleSnapshot } from "./sidecar-lock-reclaim.js";
|
|
3
|
+
export type SidecarLockRetryOptions = {
|
|
4
|
+
retries?: number;
|
|
5
|
+
factor?: number;
|
|
6
|
+
minTimeout?: number;
|
|
7
|
+
maxTimeout?: number;
|
|
8
|
+
randomize?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type SidecarLockStaleRecovery = "fail-closed" | "remove-if-unchanged";
|
|
11
|
+
export type SidecarLockCompromisedInfo = {
|
|
12
|
+
lockPath: string;
|
|
13
|
+
normalizedTargetPath: string;
|
|
14
|
+
};
|
|
15
|
+
export type SidecarLockAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
16
|
+
targetPath: string;
|
|
17
|
+
lockPath?: string;
|
|
18
|
+
staleMs: number;
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
retry?: SidecarLockRetryOptions;
|
|
21
|
+
staleRecovery?: SidecarLockStaleRecovery;
|
|
22
|
+
reentrantOwner?: string;
|
|
23
|
+
payload: () => TPayload | Promise<TPayload>;
|
|
24
|
+
shouldReclaim?: (params: {
|
|
25
|
+
lockPath: string;
|
|
26
|
+
normalizedTargetPath: string;
|
|
27
|
+
payload: unknown;
|
|
28
|
+
staleMs: number;
|
|
29
|
+
nowMs: number;
|
|
30
|
+
heldByThisProcess: boolean;
|
|
31
|
+
}) => boolean | Promise<boolean>;
|
|
32
|
+
shouldRemoveStaleLock?: (snapshot: SidecarLockStaleSnapshot) => boolean | Promise<boolean>;
|
|
33
|
+
metadata?: Record<string, unknown>;
|
|
34
|
+
parsePayload?: (raw: string) => unknown;
|
|
35
|
+
lockRoot?: Root;
|
|
36
|
+
onCompromised?: (info: SidecarLockCompromisedInfo) => void;
|
|
37
|
+
compromiseCheckIntervalMs?: number;
|
|
38
|
+
};
|
|
39
|
+
export type SidecarLockHandle = {
|
|
40
|
+
lockPath: string;
|
|
41
|
+
normalizedTargetPath: string;
|
|
42
|
+
verifyStillHeld: () => Promise<boolean>;
|
|
43
|
+
release: () => Promise<void>;
|
|
44
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
45
|
+
};
|
|
46
|
+
export type SidecarLockHeldEntry = {
|
|
47
|
+
normalizedTargetPath: string;
|
|
48
|
+
lockPath: string;
|
|
49
|
+
acquiredAt: number;
|
|
50
|
+
metadata: Record<string, unknown>;
|
|
51
|
+
forceRelease: () => Promise<boolean>;
|
|
52
|
+
};
|
|
53
|
+
export type WithSidecarLockOptions<TPayload extends Record<string, unknown>> = Omit<SidecarLockAcquireOptions<TPayload>, "targetPath"> & {
|
|
54
|
+
managerKey?: string;
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=sidecar-lock-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sidecar-lock-types.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAE1E,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,GAAG,qBAAqB,CAAC;AAE7E,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,yBAAyB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IAChF,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,uBAAuB,CAAC;IAChC,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5C,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE;QACvB,QAAQ,EAAE,MAAM,CAAC;QACjB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,iBAAiB,EAAE,OAAO,CAAC;KAC5B,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjC,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,wBAAwB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,0BAA0B,KAAK,IAAI,CAAC;IAC3D,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,YAAY,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,sBAAsB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,IAAI,CACjF,yBAAyB,CAAC,QAAQ,CAAC,EACnC,YAAY,CACb,GAAG;IACF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/sidecar-lock.d.ts
CHANGED
|
@@ -1,49 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SidecarLockAcquireOptions, SidecarLockHandle, SidecarLockHeldEntry, WithSidecarLockOptions } from "./sidecar-lock-types.js";
|
|
2
2
|
export type { SidecarLockStaleSnapshot } from "./sidecar-lock-reclaim.js";
|
|
3
|
-
export type SidecarLockRetryOptions
|
|
4
|
-
retries?: number;
|
|
5
|
-
factor?: number;
|
|
6
|
-
minTimeout?: number;
|
|
7
|
-
maxTimeout?: number;
|
|
8
|
-
randomize?: boolean;
|
|
9
|
-
};
|
|
10
|
-
export type SidecarLockStaleRecovery = "fail-closed" | "remove-if-unchanged";
|
|
11
|
-
export type SidecarLockAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
12
|
-
targetPath: string;
|
|
13
|
-
lockPath?: string;
|
|
14
|
-
staleMs: number;
|
|
15
|
-
timeoutMs?: number;
|
|
16
|
-
retry?: SidecarLockRetryOptions;
|
|
17
|
-
staleRecovery?: SidecarLockStaleRecovery;
|
|
18
|
-
allowReentrant?: boolean;
|
|
19
|
-
payload: () => TPayload | Promise<TPayload>;
|
|
20
|
-
shouldReclaim?: (params: {
|
|
21
|
-
lockPath: string;
|
|
22
|
-
normalizedTargetPath: string;
|
|
23
|
-
payload: Record<string, unknown> | null;
|
|
24
|
-
staleMs: number;
|
|
25
|
-
nowMs: number;
|
|
26
|
-
heldByThisProcess: boolean;
|
|
27
|
-
}) => boolean | Promise<boolean>;
|
|
28
|
-
shouldRemoveStaleLock?: (snapshot: SidecarLockStaleSnapshot) => boolean | Promise<boolean>;
|
|
29
|
-
metadata?: Record<string, unknown>;
|
|
30
|
-
};
|
|
31
|
-
export type SidecarLockHandle = {
|
|
32
|
-
lockPath: string;
|
|
33
|
-
normalizedTargetPath: string;
|
|
34
|
-
release: () => Promise<void>;
|
|
35
|
-
[Symbol.asyncDispose](): Promise<void>;
|
|
36
|
-
};
|
|
37
|
-
export type SidecarLockHeldEntry = {
|
|
38
|
-
normalizedTargetPath: string;
|
|
39
|
-
lockPath: string;
|
|
40
|
-
acquiredAt: number;
|
|
41
|
-
metadata: Record<string, unknown>;
|
|
42
|
-
forceRelease: () => Promise<boolean>;
|
|
43
|
-
};
|
|
44
|
-
export type WithSidecarLockOptions<TPayload extends Record<string, unknown>> = Omit<SidecarLockAcquireOptions<TPayload>, "targetPath"> & {
|
|
45
|
-
managerKey?: string;
|
|
46
|
-
};
|
|
3
|
+
export type { SidecarLockAcquireOptions, SidecarLockCompromisedInfo, SidecarLockHandle, SidecarLockHeldEntry, SidecarLockRetryOptions, SidecarLockStaleRecovery, WithSidecarLockOptions, } from "./sidecar-lock-types.js";
|
|
47
4
|
export declare function createSidecarLockManager(key: string): {
|
|
48
5
|
acquire: <TPayload extends Record<string, unknown>>(options: SidecarLockAcquireOptions<TPayload>) => Promise<SidecarLockHandle>;
|
|
49
6
|
withLock: <T, TPayload extends Record<string, unknown>>(options: SidecarLockAcquireOptions<TPayload>, fn: () => Promise<T>) => Promise<T>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EAEpB,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAKjC,YAAY,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,YAAY,EACV,yBAAyB,EACzB,0BAA0B,EAC1B,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AAsLjC,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM;cAgB3B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACpD,yBAAyB,CAAC,QAAQ,CAAC,KAC3C,OAAO,CAAC,iBAAiB,CAAC;eAgNL,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACxD,yBAAyB,CAAC,QAAQ,CAAC,MACxC,MAAM,OAAO,CAAC,CAAC,CAAC,KACnB,OAAO,CAAC,CAAC,CAAC;iBASW,OAAO,CAAC,IAAI,CAAC;iBAQnB,IAAI;uBAIE,oBAAoB,EAAE;EAW/C;AAED,wBAAsB,eAAe,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,EACzC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAMZ"}
|