@openclaw/fs-safe 0.7.1 → 0.8.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/CHANGELOG.md +71 -0
- package/README.md +5 -1
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +9 -10
- package/dist/archive-merge.d.ts +18 -0
- package/dist/archive-merge.d.ts.map +1 -0
- package/dist/archive-merge.js +157 -0
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +25 -5
- package/dist/archive-policy.d.ts +1 -1
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +1 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +3 -2
- package/dist/archive-staging.d.ts +13 -8
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +44 -192
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +9 -5
- package/dist/archive-tar-runtime.d.ts +1 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-zip-admission.d.ts +2 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +2 -2
- package/dist/archive-zip-directory.d.ts +8 -1
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +6 -2
- package/dist/archive-zip-entry.d.ts +1 -1
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-names.d.ts +1 -1
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +1 -0
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +17 -25
- package/dist/directory-guard.d.ts +17 -5
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +8 -6
- package/dist/directory-mode-node.d.ts +8 -0
- package/dist/directory-mode-node.d.ts.map +1 -0
- package/dist/directory-mode-node.js +93 -0
- package/dist/directory-mode-owner.d.ts +21 -0
- package/dist/directory-mode-owner.d.ts.map +1 -0
- package/dist/directory-mode-owner.js +73 -0
- package/dist/file-observation.d.ts +1 -1
- package/dist/file-observation.d.ts.map +1 -1
- package/dist/file-store-boundary.d.ts +2 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +13 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +8 -11
- package/dist/filename.d.ts +6 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +36 -0
- package/dist/guarded-mkdir.d.ts +2 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +2 -2
- package/dist/guarded-mutation.d.ts +3 -2
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +2 -0
- package/dist/json-document-store.d.ts +3 -0
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +2 -0
- package/dist/json-durable-queue-ownership.d.ts +3 -1
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +7 -4
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +25 -28
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +18 -56
- package/dist/move-path-cleanup.d.ts +43 -0
- package/dist/move-path-cleanup.d.ts.map +1 -0
- package/dist/move-path-cleanup.js +159 -0
- package/dist/move-path.d.ts +2 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +47 -83
- package/dist/native-operations.d.ts +2 -4
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +7 -9
- package/dist/native-pinned-write-windows.d.ts +2 -2
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +5 -5
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +14 -4
- package/dist/native-staged-file.d.ts +2 -2
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/opened-file-failure.d.ts +1 -0
- package/dist/opened-file-failure.d.ts.map +1 -1
- package/dist/opened-file-failure.js +4 -0
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +9 -3
- package/dist/pinned-write.d.ts +2 -2
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +32 -13
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +7 -3
- package/dist/regular-file.d.ts +1 -1
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +35 -6
- package/dist/replace-file-descriptor.d.ts +9 -1
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +58 -10
- package/dist/replace-file-temp-owner.d.ts +7 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +29 -0
- package/dist/replace-file.d.ts +1 -0
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +12 -38
- package/dist/root-context.d.ts +4 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +19 -0
- package/dist/root-impl.d.ts +43 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +13 -11
- package/dist/root-path-symlink.d.ts +6 -2
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +7 -6
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +6 -2
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +4 -0
- package/dist/root-write-verification.d.ts +3 -2
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +6 -0
- package/dist/secret-file.d.ts +9 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +136 -74
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +9 -3
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +18 -5
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +6 -4
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +26 -0
- package/dist/write-open-flags.d.ts +5 -0
- package/dist/write-open-flags.d.ts.map +1 -0
- package/dist/write-open-flags.js +35 -0
- package/docs/advanced.md +1 -1
- package/docs/archive.md +56 -7
- package/docs/atomic.md +42 -2
- package/docs/contributing.md +13 -0
- package/docs/durability.md +3 -2
- package/docs/errors.md +3 -1
- package/docs/file-store.md +38 -8
- package/docs/local-roots.md +4 -3
- package/docs/output.md +4 -1
- package/docs/private-file-store.md +11 -2
- package/docs/reading.md +3 -1
- package/docs/regular-file.md +16 -2
- package/docs/root.md +4 -2
- package/docs/secret-file.md +37 -2
- package/docs/sidecar-lock.md +32 -5
- package/docs/store.md +5 -1
- package/docs/temp.md +19 -1
- package/docs/writing.md +24 -3
- package/package.json +14 -10
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
2
2
|
import type { PublishedWriteIdentity } from "./pinned-write.js";
|
|
3
3
|
import { type RootContext } from "./root-context.js";
|
|
4
4
|
export declare function verifyAtomicWriteResult(params: {
|
|
@@ -6,6 +6,7 @@ export declare function verifyAtomicWriteResult(params: {
|
|
|
6
6
|
targetPath: string;
|
|
7
7
|
fd: number;
|
|
8
8
|
expectedIdentity: PublishedWriteIdentity;
|
|
9
|
-
|
|
9
|
+
expectedMode?: number;
|
|
10
|
+
parentGuard: AnyAsyncDirectoryGuard;
|
|
10
11
|
}): Promise<void>;
|
|
11
12
|
//# sourceMappingURL=root-write-verification.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,
|
|
1
|
+
{"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAK9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhF,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,sBAAsB,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,sBAAsB,CAAC;CACrC,GAAG,OAAO,CAAC,IAAI,CAAC,CAqFhB"}
|
|
@@ -35,6 +35,12 @@ export async function verifyAtomicWriteResult(params) {
|
|
|
35
35
|
throw new FsSafeError("path-mismatch", "descriptor changed during write");
|
|
36
36
|
}
|
|
37
37
|
assertFile(stat);
|
|
38
|
+
if (process.platform !== "win32" && params.expectedMode !== undefined) {
|
|
39
|
+
const actualMode = Number(stat.mode & 4095n);
|
|
40
|
+
if (actualMode !== params.expectedMode) {
|
|
41
|
+
throw new Error(`Private secret file ${params.targetPath} has insecure permissions ${actualMode.toString(8)}.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
38
44
|
return stat;
|
|
39
45
|
};
|
|
40
46
|
try {
|
package/dist/secret-file.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
import { type AsyncDirectoryGuard } from "./directory-guard.js";
|
|
1
3
|
import { type SecretFileReadOptions } from "./secret-read-policy.js";
|
|
2
4
|
export declare const PRIVATE_SECRET_DIR_MODE = 448;
|
|
3
5
|
export declare const PRIVATE_SECRET_FILE_MODE = 384;
|
|
@@ -10,6 +12,13 @@ type SecretFileWriteParams = {
|
|
|
10
12
|
mode?: number;
|
|
11
13
|
dirMode?: number;
|
|
12
14
|
};
|
|
15
|
+
export declare function prepareSecretFileWrite(params: Omit<SecretFileWriteParams, "content">): Promise<{
|
|
16
|
+
mode: number;
|
|
17
|
+
rootGuard: AsyncDirectoryGuard<BigIntStats>;
|
|
18
|
+
parentGuard: AsyncDirectoryGuard<BigIntStats>;
|
|
19
|
+
fileName: string;
|
|
20
|
+
finalFilePath: string;
|
|
21
|
+
}>;
|
|
13
22
|
export declare function writeSecretFileAtomic(params: SecretFileWriteParams): Promise<void>;
|
|
14
23
|
export declare function createSecretFileAtomic(params: SecretFileWriteParams): Promise<void>;
|
|
15
24
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM/C,OAAO,EAAkF,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAShJ,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAIjC,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAwER;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAUpB;AA8JD,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAYF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,SAAS,CAAC,GAC7C,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC5C,WAAW,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAuBD;AAuDD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAezF"}
|
package/dist/secret-file.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
1
|
+
import fs, {} from "node:fs";
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
|
|
5
5
|
import { readFileDescriptorBoundedSync } from "./bounded-read.js";
|
|
6
6
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
7
|
-
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
7
|
+
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
|
|
8
|
+
import { pinNodeDirectoryForMode } from "./directory-mode-node.js";
|
|
9
|
+
import { assertOwnedDirectory } from "./directory-mode-owner.js";
|
|
8
10
|
import { FsSafeError } from "./errors.js";
|
|
9
|
-
import { sameFileIdentity } from "./file-identity.js";
|
|
10
11
|
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
11
12
|
import { openPinnedFileSync } from "./pinned-open.js";
|
|
12
|
-
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
13
13
|
import { runPinnedWriteHelper } from "./pinned-write.js";
|
|
14
|
+
import { ensureTrailingSep } from "./root-context.js";
|
|
15
|
+
import { verifyAtomicWriteResult } from "./root-write-verification.js";
|
|
14
16
|
import { assertSecretFilePreview, DEFAULT_SECRET_FILE_MAX_BYTES, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
|
|
15
|
-
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
17
|
+
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
16
18
|
import { serializePathWrite } from "./write-queue.js";
|
|
17
19
|
export const PRIVATE_SECRET_DIR_MODE = 0o700;
|
|
18
20
|
export const PRIVATE_SECRET_FILE_MODE = 0o600;
|
|
@@ -104,95 +106,131 @@ function assertRealPathWithinRoot(rootDir, targetPath) {
|
|
|
104
106
|
throw new Error(`Private secret path must stay under ${rootDir}.`);
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
|
-
async function
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
async function createPrivateDirectory(directory, mode) {
|
|
110
|
+
try {
|
|
111
|
+
await fsp.mkdir(directory, { mode });
|
|
112
|
+
return true;
|
|
110
113
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
throw new Error(`Private secret ${kind} ${resolvedPath} has insecure permissions ${actualMode.toString(8)}.`);
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error.code === "EEXIST")
|
|
116
|
+
return false;
|
|
117
|
+
throw error;
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
|
-
async function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (pathStat.isSymbolicLink() || !sameFileIdentity(pathStat, openedStat)) {
|
|
127
|
-
throw new FsSafeError("path-mismatch", "private secret path changed during write");
|
|
128
|
-
}
|
|
129
|
-
if (process.platform !== "win32") {
|
|
130
|
-
await handle.chmod(expectedMode);
|
|
131
|
-
const chmodStat = await handle.stat();
|
|
132
|
-
const actualMode = chmodStat.mode & 0o777;
|
|
133
|
-
if (actualMode !== expectedMode) {
|
|
134
|
-
throw new Error(`Private secret file ${resolvedPath} has insecure permissions ${actualMode.toString(8)}.`);
|
|
135
|
-
}
|
|
136
|
-
const refreshedPathStat = await fsp.lstat(resolvedPath);
|
|
137
|
-
if (refreshedPathStat.isSymbolicLink() || !sameFileIdentity(refreshedPathStat, chmodStat)) {
|
|
138
|
-
throw new FsSafeError("path-mismatch", "private secret path changed during mode check");
|
|
139
|
-
}
|
|
120
|
+
async function enforcePrivateDirectoryMode(params) {
|
|
121
|
+
if (process.platform === "win32")
|
|
122
|
+
return;
|
|
123
|
+
const stat = await inspectDirectoryIdentity(params.realPath, params.identity);
|
|
124
|
+
if (!params.created) {
|
|
125
|
+
const actualMode = Number(stat.mode & 4095n);
|
|
126
|
+
if (actualMode !== params.mode) {
|
|
127
|
+
throw new FsSafeError("insecure-permissions", `Private secret directory ${JSON.stringify(params.realPath)} has insecure permissions ${actualMode.toString(8)}.`);
|
|
140
128
|
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const ownerUid = process.geteuid?.();
|
|
132
|
+
if (ownerUid === undefined) {
|
|
133
|
+
throw new FsSafeError("helper-unavailable", "secret directory initialization requires owner identity");
|
|
134
|
+
}
|
|
135
|
+
const owner = await pinNodeDirectoryForMode(params.realPath, {
|
|
136
|
+
expectedIdentity: params.identity,
|
|
137
|
+
ownerUid,
|
|
138
|
+
});
|
|
139
|
+
try {
|
|
140
|
+
await owner.apply(params.mode, { beforeChmod: params.beforeChmod });
|
|
141
141
|
}
|
|
142
142
|
finally {
|
|
143
|
-
await
|
|
143
|
+
await owner.close();
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
async function inspectPrivateDirectory(directory, kind) {
|
|
147
|
+
return await inspectFileIdentity(async () => {
|
|
148
|
+
const stat = await fsp.lstat(directory, { bigint: true });
|
|
149
|
+
if (stat.isSymbolicLink()) {
|
|
150
|
+
throw new Error(`Private secret ${kind} ${directory} must not be a symlink.`);
|
|
151
|
+
}
|
|
152
|
+
if (!stat.isDirectory()) {
|
|
153
|
+
throw new Error(`Private secret ${kind} ${directory} must be a directory.`);
|
|
154
|
+
}
|
|
155
|
+
return stat;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
146
158
|
async function ensurePrivateDirectory(rootDir, targetDir, mode) {
|
|
147
159
|
const resolvedRoot = path.resolve(rootDir);
|
|
148
160
|
const resolvedTarget = path.resolve(targetDir);
|
|
149
|
-
await
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
let rootStat = await inspectPrivateDirectory(resolvedRoot, "root").catch((error) => {
|
|
162
|
+
if (error.code !== "ENOENT")
|
|
163
|
+
throw error;
|
|
164
|
+
return undefined;
|
|
165
|
+
});
|
|
166
|
+
let createdRoot = false;
|
|
167
|
+
if (!rootStat) {
|
|
168
|
+
try {
|
|
169
|
+
createdRoot = await createPrivateDirectory(resolvedRoot, mode);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error.code !== "ENOENT")
|
|
173
|
+
throw error;
|
|
174
|
+
await fsp.mkdir(path.dirname(resolvedRoot), { recursive: true, mode });
|
|
175
|
+
createdRoot = await createPrivateDirectory(resolvedRoot, mode);
|
|
176
|
+
}
|
|
177
|
+
rootStat = await inspectPrivateDirectory(resolvedRoot, "root");
|
|
156
178
|
}
|
|
157
|
-
const rootGuard = await createAsyncDirectoryGuard(resolvedRoot);
|
|
158
|
-
|
|
179
|
+
const rootGuard = await createAsyncDirectoryGuard(resolvedRoot, { bigint: true });
|
|
180
|
+
assertOwnedDirectory(rootStat, rootGuard.stat);
|
|
181
|
+
await enforcePrivateDirectoryMode({
|
|
182
|
+
realPath: rootGuard.realPath, identity: rootStat, mode, created: createdRoot,
|
|
183
|
+
beforeChmod: () => assertAsyncDirectoryGuard(rootGuard),
|
|
184
|
+
});
|
|
159
185
|
await assertAsyncDirectoryGuard(rootGuard);
|
|
160
186
|
if (resolvedTarget === resolvedRoot) {
|
|
161
|
-
return { rootGuard,
|
|
187
|
+
return { rootGuard, parentGuard: rootGuard };
|
|
162
188
|
}
|
|
163
189
|
assertPathWithinRoot(resolvedRoot, resolvedTarget);
|
|
164
190
|
const resolvedRootReal = rootGuard.realPath;
|
|
165
191
|
let current = resolvedRoot;
|
|
192
|
+
let targetGuard = rootGuard;
|
|
166
193
|
for (const segment of path
|
|
167
194
|
.relative(resolvedRoot, resolvedTarget)
|
|
168
195
|
.split(path.sep)
|
|
169
196
|
.filter(Boolean)) {
|
|
170
197
|
current = path.join(current, segment);
|
|
171
|
-
const parentGuard =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
198
|
+
const parentGuard = targetGuard;
|
|
199
|
+
let created = false;
|
|
200
|
+
let identity;
|
|
201
|
+
while (true) {
|
|
202
|
+
await assertAsyncDirectoryGuard(rootGuard);
|
|
203
|
+
await assertAsyncDirectoryGuard(parentGuard);
|
|
204
|
+
try {
|
|
205
|
+
identity = await inspectPrivateDirectory(current, "directory component");
|
|
206
|
+
break;
|
|
180
207
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
208
|
+
catch (error) {
|
|
209
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
await assertAsyncDirectoryGuard(parentGuard);
|
|
213
|
+
created = await createPrivateDirectory(current, mode);
|
|
214
|
+
// EEXIST grants no initialization authority; both outcomes need fresh type checks.
|
|
185
215
|
}
|
|
186
|
-
await assertAsyncDirectoryGuard(parentGuard);
|
|
187
|
-
await fsp.mkdir(current, { mode });
|
|
188
216
|
}
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
217
|
+
const currentGuard = await createAsyncDirectoryGuard(current, { bigint: true });
|
|
218
|
+
assertOwnedDirectory(identity, currentGuard.stat);
|
|
219
|
+
assertRealPathWithinRoot(resolvedRootReal, currentGuard.realPath);
|
|
220
|
+
await enforcePrivateDirectoryMode({
|
|
221
|
+
realPath: currentGuard.realPath, identity, mode, created,
|
|
222
|
+
beforeChmod: async () => {
|
|
223
|
+
await assertAsyncDirectoryGuard(parentGuard);
|
|
224
|
+
await assertAsyncDirectoryGuard(rootGuard);
|
|
225
|
+
await assertAsyncDirectoryGuard(currentGuard);
|
|
226
|
+
},
|
|
227
|
+
});
|
|
192
228
|
await assertAsyncDirectoryGuard(parentGuard);
|
|
193
229
|
await assertAsyncDirectoryGuard(rootGuard);
|
|
230
|
+
await assertAsyncDirectoryGuard(currentGuard);
|
|
231
|
+
targetGuard = currentGuard;
|
|
194
232
|
}
|
|
195
|
-
return { rootGuard,
|
|
233
|
+
return { rootGuard, parentGuard: targetGuard };
|
|
196
234
|
}
|
|
197
235
|
async function secretFileWriteQueueKey(filePath) {
|
|
198
236
|
try {
|
|
@@ -203,19 +241,29 @@ async function secretFileWriteQueueKey(filePath) {
|
|
|
203
241
|
return path.resolve(filePath);
|
|
204
242
|
}
|
|
205
243
|
}
|
|
206
|
-
|
|
244
|
+
// Internal preparation for private writers and their pre-write locks; not lasting authorization.
|
|
245
|
+
export async function prepareSecretFileWrite(params) {
|
|
207
246
|
const mode = params.mode ?? PRIVATE_SECRET_FILE_MODE;
|
|
208
247
|
const dirMode = params.dirMode ?? PRIVATE_SECRET_DIR_MODE;
|
|
209
248
|
const resolvedRoot = path.resolve(params.rootDir);
|
|
210
249
|
const resolvedFile = path.resolve(params.filePath);
|
|
211
250
|
assertPathWithinRoot(resolvedRoot, resolvedFile);
|
|
251
|
+
for (const [kind, value] of [["file", mode], ["directory", dirMode]]) {
|
|
252
|
+
if (!Number.isInteger(value) || value < 0 || value > 0o7777) {
|
|
253
|
+
throw new FsSafeError("invalid-path", `Private secret ${kind} mode must be an integer between 0o0000 and 0o7777.`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
212
256
|
const intendedParentDir = path.dirname(resolvedFile);
|
|
213
|
-
const { rootGuard,
|
|
257
|
+
const { rootGuard, parentGuard } = await ensurePrivateDirectory(resolvedRoot, intendedParentDir, dirMode);
|
|
214
258
|
await assertAsyncDirectoryGuard(rootGuard);
|
|
215
|
-
|
|
216
|
-
|
|
259
|
+
await assertAsyncDirectoryGuard(parentGuard);
|
|
260
|
+
assertRealPathWithinRoot(rootGuard.realPath, parentGuard.realPath);
|
|
217
261
|
const fileName = path.basename(resolvedFile);
|
|
218
|
-
const finalFilePath = path.join(
|
|
262
|
+
const finalFilePath = path.join(parentGuard.realPath, fileName);
|
|
263
|
+
return { mode, rootGuard, parentGuard, fileName, finalFilePath };
|
|
264
|
+
}
|
|
265
|
+
async function materializeSecretFileAtomic(params, createOnly) {
|
|
266
|
+
const { mode, rootGuard, parentGuard, fileName, finalFilePath } = await prepareSecretFileWrite(params);
|
|
219
267
|
try {
|
|
220
268
|
const stat = await fsp.lstat(finalFilePath);
|
|
221
269
|
if (createOnly) {
|
|
@@ -235,7 +283,7 @@ async function materializeSecretFileAtomic(params, createOnly) {
|
|
|
235
283
|
}
|
|
236
284
|
await assertAsyncDirectoryGuard(rootGuard);
|
|
237
285
|
await assertAsyncDirectoryGuard(parentGuard);
|
|
238
|
-
|
|
286
|
+
await runPinnedWriteHelper({
|
|
239
287
|
rootPath: parentGuard.realPath,
|
|
240
288
|
relativeParentPath: "",
|
|
241
289
|
basename: fileName,
|
|
@@ -244,10 +292,24 @@ async function materializeSecretFileAtomic(params, createOnly) {
|
|
|
244
292
|
overwrite: !createOnly,
|
|
245
293
|
input: { kind: "buffer", data: typeof params.content === "string" ? params.content : Buffer.from(params.content) },
|
|
246
294
|
rootIdentity: { dev: parentGuard.stat.dev, ino: parentGuard.stat.ino },
|
|
295
|
+
verifyPublished: async (fd, expectedIdentity, publishedParentGuard) => {
|
|
296
|
+
await assertAsyncDirectoryGuard(rootGuard);
|
|
297
|
+
await assertAsyncDirectoryGuard(parentGuard);
|
|
298
|
+
await verifyAtomicWriteResult({
|
|
299
|
+
root: {
|
|
300
|
+
rootDir: rootGuard.dir,
|
|
301
|
+
rootReal: rootGuard.realPath,
|
|
302
|
+
rootWithSep: ensureTrailingSep(rootGuard.realPath),
|
|
303
|
+
rootIdentity: { dev: rootGuard.stat.dev, ino: rootGuard.stat.ino },
|
|
304
|
+
},
|
|
305
|
+
targetPath: finalFilePath,
|
|
306
|
+
fd,
|
|
307
|
+
expectedIdentity,
|
|
308
|
+
expectedMode: mode,
|
|
309
|
+
parentGuard: publishedParentGuard,
|
|
310
|
+
});
|
|
311
|
+
},
|
|
247
312
|
});
|
|
248
|
-
await assertAsyncDirectoryGuard(rootGuard);
|
|
249
|
-
await assertAsyncDirectoryGuard(parentGuard);
|
|
250
|
-
await enforcePrivateFileIdentityAndMode(finalFilePath, identity, mode);
|
|
251
313
|
}
|
|
252
314
|
export async function writeSecretFileAtomic(params) {
|
|
253
315
|
const canonicalPath = await secretFileWriteQueueKey(params.filePath);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"AAaA,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,uGAAuG;IACvG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,yEAAyE;IACzE,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;AASF,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAoBxC;
|
|
1
|
+
{"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"AAaA,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,uGAAuG;IACvG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,yEAAyE;IACzE,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;AASF,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAoBxC;AAwBD,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,CA0ChB"}
|
package/dist/sibling-temp.js
CHANGED
|
@@ -2,7 +2,7 @@ import crypto, { randomUUID } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
5
|
-
import { sanitizeUntrustedFileName } from "./filename.js";
|
|
5
|
+
import { fitFileNameToPortableComponent, sanitizeUntrustedFileName } from "./filename.js";
|
|
6
6
|
import { applyDirectoryMode } from "./replace-file-descriptor.js";
|
|
7
7
|
import { root } from "./root.js";
|
|
8
8
|
import { assertSafePathPrefix } from "./safe-path-segment.js";
|
|
@@ -42,8 +42,14 @@ function buildSiblingTempPath(params) {
|
|
|
42
42
|
const safePrefix = assertSafePathPrefix(params.tempPrefix, {
|
|
43
43
|
label: "sibling temp prefix",
|
|
44
44
|
});
|
|
45
|
-
const
|
|
46
|
-
|
|
45
|
+
const prefix = `${safePrefix}${id}-`;
|
|
46
|
+
const suffix = ".part";
|
|
47
|
+
const safeTail = fitFileNameToPortableComponent({
|
|
48
|
+
prefix,
|
|
49
|
+
fileName: sanitizeUntrustedFileName(path.basename(params.targetPath), params.fallbackFileName),
|
|
50
|
+
suffix,
|
|
51
|
+
});
|
|
52
|
+
return path.join(path.dirname(params.targetPath), `${prefix}${safeTail}${suffix}`);
|
|
47
53
|
}
|
|
48
54
|
export async function writeViaSiblingTempPath(params) {
|
|
49
55
|
const rootDir = await fs
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-acquire.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-acquire.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sidecar-lock-acquire.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-acquire.ts"],"names":[],"mappings":"AAOA,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAS3C,OAAO,EAWL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAG5F,KAAK,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAEhF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;CAClC,CAAC;AAEF,KAAK,6BAA6B,GAAG;IACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACnC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,2BAA2B,IAAI,IAAI,CAAC;IACpC,iBAAiB,CAAC,oBAAoB,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,iBAAiB,CAAC;IAC1F,eAAe,CACb,oBAAoB,EAAE,MAAM,EAC5B,IAAI,EAAE,eAAe,EACrB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAC5B,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB,CAAC;AAqBF,wBAAsB,kBAAkB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,OAAO,EAAE,yBAAyB,CAAC,QAAQ,CAAC,EAC5C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,iBAAiB,CAAC,CA+S5B"}
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
|
|
3
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { fileObservation } from "./file-observation.js";
|
|
4
6
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
5
7
|
import { openSidecarRoot } from "./sidecar-lock-root.js";
|
|
6
8
|
import { createNativeExclusiveFile } from "./native-operations.js";
|
|
7
9
|
import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, isTransientLockFileDenial, maxTransientLockDenials, validateSidecarLockRetryOptions, validateSidecarLockTimeoutMs, } from "./sidecar-lock-policy.js";
|
|
8
10
|
import { readSidecarLockRawSnapshot, parseSidecarLockSnapshot, relativeSidecarLockPath, releaseSidecarReclaimGuard, removeSidecarLockIfUnchanged, removeStaleSidecarLockIfAllowed, serializeSidecarLockPayload, sidecarLockSnapshotStillPresent, sidecarReclaimGuardExists, tryAcquireSidecarReclaimGuard, } from "./sidecar-lock-reclaim.js";
|
|
9
11
|
import { createSuppressedError } from "./suppressed-error.js";
|
|
10
|
-
async function resolveNormalizedTargetPath(targetPath) {
|
|
12
|
+
async function resolveNormalizedTargetPath(targetPath, lockRoot) {
|
|
11
13
|
const resolved = path.resolve(targetPath);
|
|
12
14
|
const dir = path.dirname(resolved);
|
|
15
|
+
if (lockRoot) {
|
|
16
|
+
// The target is an arbitration key, not necessarily inside the lock Root.
|
|
17
|
+
// Leave parent creation to the Root-backed sidecar write.
|
|
18
|
+
await lockRoot.resolve(".");
|
|
19
|
+
const parent = await canonicalPathFromExistingAncestor(dir);
|
|
20
|
+
await lockRoot.resolve(".");
|
|
21
|
+
return path.join(parent, path.basename(resolved));
|
|
22
|
+
}
|
|
13
23
|
await fs.mkdir(dir, { recursive: true });
|
|
14
24
|
try {
|
|
15
25
|
return path.join(await fs.realpath(dir), path.basename(resolved));
|
|
@@ -23,7 +33,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
23
33
|
validateSidecarLockRetryOptions(retry);
|
|
24
34
|
validateSidecarLockTimeoutMs(options.timeoutMs);
|
|
25
35
|
context.ensureExitCleanupRegistered();
|
|
26
|
-
const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
|
|
36
|
+
const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath, options.lockRoot);
|
|
27
37
|
const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
|
|
28
38
|
let held = context.held.get(normalizedTargetPath);
|
|
29
39
|
if (held &&
|
|
@@ -47,7 +57,6 @@ export async function acquireSidecarLock(options, context) {
|
|
|
47
57
|
}
|
|
48
58
|
}
|
|
49
59
|
const startedAt = Date.now();
|
|
50
|
-
const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
|
|
51
60
|
const reclaimGuardPath = `${lockPath}.reclaim`;
|
|
52
61
|
let ownsReclaimGuard = false;
|
|
53
62
|
let attempt = 0;
|
|
@@ -59,7 +68,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
59
68
|
if ((options.timeoutMs !== undefined &&
|
|
60
69
|
options.timeoutMs !== Number.POSITIVE_INFINITY &&
|
|
61
70
|
elapsed >= options.timeoutMs) ||
|
|
62
|
-
(
|
|
71
|
+
(retry.retries !== undefined && attempt >= retry.retries)) {
|
|
63
72
|
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
64
73
|
code: "file_lock_timeout",
|
|
65
74
|
lockPath,
|
|
@@ -101,10 +110,14 @@ export async function acquireSidecarLock(options, context) {
|
|
|
101
110
|
if (options.lockRoot) {
|
|
102
111
|
const lockRoot = options.lockRoot;
|
|
103
112
|
const relativeLockPath = relativeSidecarLockPath(lockRoot, lockPath);
|
|
113
|
+
const observation = fileObservation();
|
|
104
114
|
try {
|
|
105
|
-
await lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
|
|
115
|
+
await observation.run(() => lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 }));
|
|
106
116
|
}
|
|
107
117
|
catch (error) {
|
|
118
|
+
// Only this invocation's failed exclusive open grants denial retry authority.
|
|
119
|
+
lockFileCreateDenied = observation.has(error, `exclusive-create:${lockPath}`) &&
|
|
120
|
+
isTransientLockFileDenial(error, lockPath);
|
|
108
121
|
if (error instanceof FsSafeError && error.code === "already-exists") {
|
|
109
122
|
throw Object.assign(new Error("sidecar lock exists"), { code: "EEXIST" });
|
|
110
123
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"AACA,OAAe,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAO7C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAa3C,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,KAAK,sBAAsB,GAAG,IAAI,CAAC,mBAAmB,EAAE,SAAS,CAAC,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAErF,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,sBAAsB,GAAG,IAAI,EACvC,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,GAChC,mBAAmB,GAAG,IAAI,CAE5B;AAYD,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE7E;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAC7E,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,CAAC;CACxB,
|
|
1
|
+
{"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"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"}
|
|
@@ -30,10 +30,12 @@ export function readSidecarLockOwnershipToken(raw) {
|
|
|
30
30
|
}
|
|
31
31
|
export function serializeSidecarLockPayload(payload) {
|
|
32
32
|
const ownershipToken = createSidecarLockOwnershipToken();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const raw = `${JSON.stringify(payload, null, 2)}\n${ownershipToken}\n`;
|
|
34
|
+
const bytes = Buffer.byteLength(raw, "utf8");
|
|
35
|
+
if (bytes > MAX_LOCK_PAYLOAD_BYTES) {
|
|
36
|
+
throw new FsSafeError("too-large", `sidecar lock payload exceeds limit of ${MAX_LOCK_PAYLOAD_BYTES} bytes (got ${bytes})`);
|
|
37
|
+
}
|
|
38
|
+
return { raw, ownershipToken };
|
|
37
39
|
}
|
|
38
40
|
export function relativeSidecarLockPath(lockRoot, lockPath) {
|
|
39
41
|
const resolved = path.resolve(lockPath);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EACpB,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AACjC,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;
|
|
1
|
+
{"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EACpB,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AACjC,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;AAmNjC,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM;cAU3B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACpD,yBAAyB,CAAC,QAAQ,CAAC,KAC3C,OAAO,CAAC,iBAAiB,CAAC;eAYL,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;iBAqBW,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"}
|
package/dist/sidecar-lock.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createSuppressedError } from "./suppressed-error.js";
|
|
|
7
7
|
const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
|
|
8
8
|
const GLOBAL_CLEANUP_KEY = Symbol.for("fsSafe.sidecarLockCleanupRegistered");
|
|
9
9
|
const GLOBAL_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.sidecarLockCleanupHandler");
|
|
10
|
+
const GLOBAL_BEFORE_EXIT_KEY = Symbol.for("fsSafe.sidecarLockBeforeExitCleanup");
|
|
10
11
|
function getGlobalManagers() {
|
|
11
12
|
const globalWithState = globalThis;
|
|
12
13
|
if (!globalWithState[GLOBAL_STATE_KEY]) {
|
|
@@ -117,6 +118,29 @@ function ensureGlobalExitCleanupRegistered() {
|
|
|
117
118
|
globalWithCleanup[GLOBAL_CLEANUP_HANDLER_KEY] = cleanup;
|
|
118
119
|
process.on("exit", cleanup);
|
|
119
120
|
}
|
|
121
|
+
function ensureGlobalBeforeExitCleanupRegistered() {
|
|
122
|
+
const globalWithCleanup = globalThis;
|
|
123
|
+
if (globalWithCleanup[GLOBAL_BEFORE_EXIT_KEY]) {
|
|
124
|
+
return globalWithCleanup[GLOBAL_BEFORE_EXIT_KEY];
|
|
125
|
+
}
|
|
126
|
+
// Independent of the exit registration, which an older package copy may own.
|
|
127
|
+
const lifecycle = { armed: false };
|
|
128
|
+
globalWithCleanup[GLOBAL_BEFORE_EXIT_KEY] = lifecycle;
|
|
129
|
+
process.on("beforeExit", () => {
|
|
130
|
+
if (!lifecycle.armed)
|
|
131
|
+
return;
|
|
132
|
+
// Cleanup itself schedules I/O; retry only after another acquisition.
|
|
133
|
+
lifecycle.armed = false;
|
|
134
|
+
for (const state of getGlobalManagers().values()) {
|
|
135
|
+
for (const [normalizedTargetPath, held] of Array.from(state.held.entries())) {
|
|
136
|
+
if (held.lockRoot) {
|
|
137
|
+
void releaseHeldLock(state, normalizedTargetPath, held, { force: true }).catch(() => undefined);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
return lifecycle;
|
|
143
|
+
}
|
|
120
144
|
async function releaseHeldLock(state, normalizedTargetPath, held, options = {}) {
|
|
121
145
|
const current = state.held.get(normalizedTargetPath);
|
|
122
146
|
if (current !== held) {
|
|
@@ -158,6 +182,7 @@ async function releaseHeldLock(state, normalizedTargetPath, held, options = {})
|
|
|
158
182
|
}
|
|
159
183
|
}
|
|
160
184
|
function handleForHeldLock(state, normalizedTargetPath, held) {
|
|
185
|
+
ensureGlobalBeforeExitCleanupRegistered().armed = true;
|
|
161
186
|
return createHeldSidecarLockHandle({
|
|
162
187
|
normalizedTargetPath,
|
|
163
188
|
held,
|
|
@@ -170,6 +195,7 @@ export function createSidecarLockManager(key) {
|
|
|
170
195
|
state.cleanupRegistered = true;
|
|
171
196
|
state.reclaimCleanupRegistered = true;
|
|
172
197
|
ensureGlobalExitCleanupRegistered();
|
|
198
|
+
ensureGlobalBeforeExitCleanupRegistered().armed = true;
|
|
173
199
|
}
|
|
174
200
|
async function acquire(options) {
|
|
175
201
|
return await acquireSidecarLock(options, {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
export declare function resolveNonblockingWriteFlag(constants?: Partial<Pick<typeof fsSync.constants, "O_NONBLOCK">>): number;
|
|
3
|
+
export declare function isNonRegularWriteOpenError(error: unknown, filePath: string, flags: number): Promise<boolean>;
|
|
4
|
+
export declare function isNonRegularWriteOpenErrorSync(error: unknown, filePath: string, flags: number): boolean;
|
|
5
|
+
//# sourceMappingURL=write-open-flags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-open-flags.d.ts","sourceRoot":"","sources":["../src/write-open-flags.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAI7B,wBAAgB,2BAA2B,CACzC,SAAS,GAAE,OAAO,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,CAAoB,GACjF,MAAM,CAIR;AAUD,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAOT"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { hasNodeErrorCode } from "./path.js";
|
|
4
|
+
export function resolveNonblockingWriteFlag(constants = fsSync.constants) {
|
|
5
|
+
return process.platform !== "win32" && typeof constants.O_NONBLOCK === "number"
|
|
6
|
+
? constants.O_NONBLOCK
|
|
7
|
+
: 0;
|
|
8
|
+
}
|
|
9
|
+
function isNonblockingWriteEnxio(error, flags) {
|
|
10
|
+
return hasNodeErrorCode(error, "ENXIO") &&
|
|
11
|
+
(flags & (fsSync.constants.O_WRONLY | fsSync.constants.O_RDWR)) === fsSync.constants.O_WRONLY &&
|
|
12
|
+
(flags & resolveNonblockingWriteFlag()) !== 0;
|
|
13
|
+
}
|
|
14
|
+
// A no-reader FIFO rejects a nonblocking write-only open before fstat is possible.
|
|
15
|
+
// Reclassify only a confirmed non-regular path; inconclusive races keep the errno.
|
|
16
|
+
export async function isNonRegularWriteOpenError(error, filePath, flags) {
|
|
17
|
+
if (!isNonblockingWriteEnxio(error, flags))
|
|
18
|
+
return false;
|
|
19
|
+
try {
|
|
20
|
+
return !(await fs.lstat(filePath)).isFile();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function isNonRegularWriteOpenErrorSync(error, filePath, flags) {
|
|
27
|
+
if (!isNonblockingWriteEnxio(error, flags))
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
return !fsSync.lstatSync(filePath).isFile();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
package/docs/advanced.md
CHANGED
|
@@ -49,7 +49,7 @@ root-path result helpers.
|
|
|
49
49
|
| `assertAbsolutePathInput` | – | Validate a caller-supplied absolute path string. |
|
|
50
50
|
| `ensureAbsoluteDirectory`, `EnsureAbsoluteDirectoryOptions`, `EnsureAbsoluteDirectoryResult` | – | Create a trusted absolute directory path one segment at a time, rejecting symlink or non-directory segments. |
|
|
51
51
|
| `canonicalPathFromExistingAncestor`, `findExistingAncestor` | – | Canonicalize without requiring the leaf to exist. |
|
|
52
|
-
| `resolveAbsolutePathForRead`, `resolveAbsolutePathForWrite`, `ResolvedAbsolutePath`, `ResolvedWritableAbsolutePath`, `AbsolutePathSymlinkPolicy` | – | Validate an absolute path against a symlink policy before opening. |
|
|
52
|
+
| `resolveAbsolutePathForRead`, `resolveAbsolutePathForWrite`, `ResolvedAbsolutePath`, `ResolvedWritableAbsolutePath`, `AbsolutePathSymlinkPolicy` | – | Validate an absolute path against a symlink policy before opening; strict writes reject dangling symlink components rather than treating them as missing. |
|
|
53
53
|
|
|
54
54
|
`ensureAbsoluteDirectory()` is for paths you already intend to trust as absolute
|
|
55
55
|
locations, such as a configured output root. It does not enforce a root boundary;
|