@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.
Files changed (157) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +5 -1
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +9 -10
  5. package/dist/archive-merge.d.ts +18 -0
  6. package/dist/archive-merge.d.ts.map +1 -0
  7. package/dist/archive-merge.js +157 -0
  8. package/dist/archive-native.d.ts.map +1 -1
  9. package/dist/archive-native.js +25 -5
  10. package/dist/archive-policy.d.ts +1 -1
  11. package/dist/archive-policy.d.ts.map +1 -1
  12. package/dist/archive-policy.js +1 -1
  13. package/dist/archive-read.d.ts.map +1 -1
  14. package/dist/archive-read.js +3 -2
  15. package/dist/archive-staging.d.ts +13 -8
  16. package/dist/archive-staging.d.ts.map +1 -1
  17. package/dist/archive-staging.js +44 -192
  18. package/dist/archive-tar-meta.d.ts.map +1 -1
  19. package/dist/archive-tar-meta.js +9 -5
  20. package/dist/archive-tar-runtime.d.ts +1 -0
  21. package/dist/archive-tar-runtime.d.ts.map +1 -1
  22. package/dist/archive-zip-admission.d.ts +2 -1
  23. package/dist/archive-zip-admission.d.ts.map +1 -1
  24. package/dist/archive-zip-admission.js +2 -2
  25. package/dist/archive-zip-directory.d.ts +8 -1
  26. package/dist/archive-zip-directory.d.ts.map +1 -1
  27. package/dist/archive-zip-directory.js +6 -2
  28. package/dist/archive-zip-entry.d.ts +1 -1
  29. package/dist/archive-zip-entry.d.ts.map +1 -1
  30. package/dist/archive-zip-names.d.ts +1 -1
  31. package/dist/archive-zip-names.d.ts.map +1 -1
  32. package/dist/archive-zip-names.js +1 -0
  33. package/dist/archive.d.ts.map +1 -1
  34. package/dist/archive.js +17 -25
  35. package/dist/directory-guard.d.ts +17 -5
  36. package/dist/directory-guard.d.ts.map +1 -1
  37. package/dist/directory-guard.js +8 -6
  38. package/dist/directory-mode-node.d.ts +8 -0
  39. package/dist/directory-mode-node.d.ts.map +1 -0
  40. package/dist/directory-mode-node.js +93 -0
  41. package/dist/directory-mode-owner.d.ts +21 -0
  42. package/dist/directory-mode-owner.d.ts.map +1 -0
  43. package/dist/directory-mode-owner.js +73 -0
  44. package/dist/file-observation.d.ts +1 -1
  45. package/dist/file-observation.d.ts.map +1 -1
  46. package/dist/file-store-boundary.d.ts +2 -0
  47. package/dist/file-store-boundary.d.ts.map +1 -1
  48. package/dist/file-store-boundary.js +13 -0
  49. package/dist/file-store.d.ts.map +1 -1
  50. package/dist/file-store.js +8 -11
  51. package/dist/filename.d.ts +6 -0
  52. package/dist/filename.d.ts.map +1 -1
  53. package/dist/filename.js +36 -0
  54. package/dist/guarded-mkdir.d.ts +2 -0
  55. package/dist/guarded-mkdir.d.ts.map +1 -1
  56. package/dist/guarded-mkdir.js +2 -2
  57. package/dist/guarded-mutation.d.ts +3 -2
  58. package/dist/guarded-mutation.d.ts.map +1 -1
  59. package/dist/guarded-mutation.js +2 -0
  60. package/dist/json-document-store.d.ts +3 -0
  61. package/dist/json-document-store.d.ts.map +1 -1
  62. package/dist/json-document-store.js +2 -0
  63. package/dist/json-durable-queue-ownership.d.ts +3 -1
  64. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  65. package/dist/json-durable-queue-ownership.js +7 -4
  66. package/dist/json-durable-queue.d.ts.map +1 -1
  67. package/dist/json-durable-queue.js +25 -28
  68. package/dist/local-roots.d.ts.map +1 -1
  69. package/dist/local-roots.js +18 -56
  70. package/dist/move-path-cleanup.d.ts +43 -0
  71. package/dist/move-path-cleanup.d.ts.map +1 -0
  72. package/dist/move-path-cleanup.js +159 -0
  73. package/dist/move-path.d.ts +2 -0
  74. package/dist/move-path.d.ts.map +1 -1
  75. package/dist/move-path.js +47 -83
  76. package/dist/native-operations.d.ts +2 -4
  77. package/dist/native-operations.d.ts.map +1 -1
  78. package/dist/native-operations.js +7 -9
  79. package/dist/native-pinned-write-windows.d.ts +2 -2
  80. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  81. package/dist/native-pinned-write-windows.js +5 -5
  82. package/dist/native-pinned-write.d.ts.map +1 -1
  83. package/dist/native-pinned-write.js +14 -4
  84. package/dist/native-staged-file.d.ts +2 -2
  85. package/dist/native-staged-file.d.ts.map +1 -1
  86. package/dist/opened-file-failure.d.ts +1 -0
  87. package/dist/opened-file-failure.d.ts.map +1 -1
  88. package/dist/opened-file-failure.js +4 -0
  89. package/dist/output-sibling.d.ts.map +1 -1
  90. package/dist/output-sibling.js +9 -3
  91. package/dist/pinned-write.d.ts +2 -2
  92. package/dist/pinned-write.d.ts.map +1 -1
  93. package/dist/pinned-write.js +32 -13
  94. package/dist/publish-file.d.ts.map +1 -1
  95. package/dist/publish-file.js +7 -3
  96. package/dist/regular-file.d.ts +1 -1
  97. package/dist/regular-file.d.ts.map +1 -1
  98. package/dist/regular-file.js +35 -6
  99. package/dist/replace-file-descriptor.d.ts +9 -1
  100. package/dist/replace-file-descriptor.d.ts.map +1 -1
  101. package/dist/replace-file-descriptor.js +58 -10
  102. package/dist/replace-file-temp-owner.d.ts +7 -0
  103. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  104. package/dist/replace-file-temp-owner.js +29 -0
  105. package/dist/replace-file.d.ts +1 -0
  106. package/dist/replace-file.d.ts.map +1 -1
  107. package/dist/replace-file.js +12 -38
  108. package/dist/root-context.d.ts +4 -0
  109. package/dist/root-context.d.ts.map +1 -1
  110. package/dist/root-context.js +19 -0
  111. package/dist/root-impl.d.ts +43 -0
  112. package/dist/root-impl.d.ts.map +1 -1
  113. package/dist/root-impl.js +13 -11
  114. package/dist/root-path-symlink.d.ts +6 -2
  115. package/dist/root-path-symlink.d.ts.map +1 -1
  116. package/dist/root-path-symlink.js +7 -6
  117. package/dist/root-path.d.ts +1 -0
  118. package/dist/root-path.d.ts.map +1 -1
  119. package/dist/root-path.js +6 -2
  120. package/dist/root-walk.d.ts.map +1 -1
  121. package/dist/root-walk.js +4 -0
  122. package/dist/root-write-verification.d.ts +3 -2
  123. package/dist/root-write-verification.d.ts.map +1 -1
  124. package/dist/root-write-verification.js +6 -0
  125. package/dist/secret-file.d.ts +9 -0
  126. package/dist/secret-file.d.ts.map +1 -1
  127. package/dist/secret-file.js +136 -74
  128. package/dist/sibling-temp.d.ts.map +1 -1
  129. package/dist/sibling-temp.js +9 -3
  130. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  131. package/dist/sidecar-lock-acquire.js +18 -5
  132. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  133. package/dist/sidecar-lock-reclaim.js +6 -4
  134. package/dist/sidecar-lock.d.ts.map +1 -1
  135. package/dist/sidecar-lock.js +26 -0
  136. package/dist/write-open-flags.d.ts +5 -0
  137. package/dist/write-open-flags.d.ts.map +1 -0
  138. package/dist/write-open-flags.js +35 -0
  139. package/docs/advanced.md +1 -1
  140. package/docs/archive.md +56 -7
  141. package/docs/atomic.md +42 -2
  142. package/docs/contributing.md +13 -0
  143. package/docs/durability.md +3 -2
  144. package/docs/errors.md +3 -1
  145. package/docs/file-store.md +38 -8
  146. package/docs/local-roots.md +4 -3
  147. package/docs/output.md +4 -1
  148. package/docs/private-file-store.md +11 -2
  149. package/docs/reading.md +3 -1
  150. package/docs/regular-file.md +16 -2
  151. package/docs/root.md +4 -2
  152. package/docs/secret-file.md +37 -2
  153. package/docs/sidecar-lock.md +32 -5
  154. package/docs/store.md +5 -1
  155. package/docs/temp.md +19 -1
  156. package/docs/writing.md +24 -3
  157. package/package.json +14 -10
@@ -1,4 +1,4 @@
1
- import { type AsyncDirectoryGuard } from "./directory-guard.js";
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
- parentGuard: AsyncDirectoryGuard;
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,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAK3F,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,WAAW,EAAE,mBAAmB,CAAC;CAClC,GAAG,OAAO,CAAC,IAAI,CAAC,CA6EhB"}
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 {
@@ -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":"AAaA,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;AAkID,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;AAkEF,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"}
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"}
@@ -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 enforcePrivatePathMode(resolvedPath, expectedMode, kind) {
108
- if (process.platform === "win32") {
109
- return;
109
+ async function createPrivateDirectory(directory, mode) {
110
+ try {
111
+ await fsp.mkdir(directory, { mode });
112
+ return true;
110
113
  }
111
- await fsp.chmod(resolvedPath, expectedMode);
112
- const stat = await fsp.stat(resolvedPath);
113
- const actualMode = stat.mode & 0o777;
114
- if (actualMode !== expectedMode) {
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 enforcePrivateFileIdentityAndMode(resolvedPath, expectedIdentity, expectedMode) {
119
- const handle = await fsp.open(resolvedPath, resolveReadOpenFlags());
120
- try {
121
- const openedStat = await handle.stat();
122
- if (!openedStat.isFile() || !sameFileIdentity(openedStat, expectedIdentity)) {
123
- throw new FsSafeError("path-mismatch", "private secret file changed during write");
124
- }
125
- const pathStat = await fsp.lstat(resolvedPath);
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 handle.close().catch(() => undefined);
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 fsp.mkdir(resolvedRoot, { recursive: true, mode });
150
- const rootStat = await fsp.lstat(resolvedRoot);
151
- if (rootStat.isSymbolicLink()) {
152
- throw new Error(`Private secret root ${resolvedRoot} must not be a symlink.`);
153
- }
154
- if (!rootStat.isDirectory()) {
155
- throw new Error(`Private secret root ${resolvedRoot} must be a directory.`);
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
- await enforcePrivatePathMode(rootGuard.realPath, mode, "directory");
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, targetReal: rootGuard.realPath };
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 = await createAsyncDirectoryGuard(path.dirname(current));
172
- await assertAsyncDirectoryGuard(rootGuard);
173
- try {
174
- const stat = await fsp.lstat(current);
175
- if (stat.isSymbolicLink()) {
176
- throw new Error(`Private secret directory component ${current} must not be a symlink.`);
177
- }
178
- if (!stat.isDirectory()) {
179
- throw new Error(`Private secret directory component ${current} must be a directory.`);
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
- catch (error) {
183
- if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
184
- throw error;
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 currentReal = await fsp.realpath(current);
190
- assertRealPathWithinRoot(resolvedRootReal, currentReal);
191
- await enforcePrivatePathMode(currentReal, mode, "directory");
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, targetReal: await fsp.realpath(resolvedTarget) };
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
- async function materializeSecretFileAtomic(params, createOnly) {
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, targetReal } = await ensurePrivateDirectory(resolvedRoot, intendedParentDir, dirMode);
257
+ const { rootGuard, parentGuard } = await ensurePrivateDirectory(resolvedRoot, intendedParentDir, dirMode);
214
258
  await assertAsyncDirectoryGuard(rootGuard);
215
- assertRealPathWithinRoot(rootGuard.realPath, targetReal);
216
- const parentGuard = await createAsyncDirectoryGuard(targetReal);
259
+ await assertAsyncDirectoryGuard(parentGuard);
260
+ assertRealPathWithinRoot(rootGuard.realPath, parentGuard.realPath);
217
261
  const fileName = path.basename(resolvedFile);
218
- const finalFilePath = path.join(targetReal, fileName);
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
- const identity = await runPinnedWriteHelper({
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;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,CA0ChB"}
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"}
@@ -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 safeTail = sanitizeUntrustedFileName(path.basename(params.targetPath), params.fallbackFileName);
46
- return path.join(path.dirname(params.targetPath), `${safePrefix}${id}-${safeTail}.part`);
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":"AAKA,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;AAaF,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,CA4S5B"}
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
- (maxRetries !== undefined && attempt >= maxRetries)) {
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":"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"}
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
- return {
34
- raw: `${JSON.stringify(payload, null, 2)}\n${ownershipToken}\n`,
35
- ownershipToken,
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;AAsLjC,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM;cAS3B,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"}
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"}
@@ -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;