@openclaw/fs-safe 0.7.2 → 0.8.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.
Files changed (159) 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 +2 -2
  58. package/dist/guarded-mutation.d.ts.map +1 -1
  59. package/dist/json-document-store.d.ts +3 -0
  60. package/dist/json-document-store.d.ts.map +1 -1
  61. package/dist/json-document-store.js +2 -0
  62. package/dist/json-durable-queue-ownership.d.ts +3 -1
  63. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  64. package/dist/json-durable-queue-ownership.js +7 -4
  65. package/dist/json-durable-queue.d.ts.map +1 -1
  66. package/dist/json-durable-queue.js +25 -28
  67. package/dist/local-roots.d.ts.map +1 -1
  68. package/dist/local-roots.js +18 -56
  69. package/dist/move-path-cleanup.d.ts +43 -0
  70. package/dist/move-path-cleanup.d.ts.map +1 -0
  71. package/dist/move-path-cleanup.js +159 -0
  72. package/dist/move-path.d.ts.map +1 -1
  73. package/dist/move-path.js +30 -84
  74. package/dist/native-operations.d.ts +2 -4
  75. package/dist/native-operations.d.ts.map +1 -1
  76. package/dist/native-operations.js +7 -9
  77. package/dist/native-pinned-write-windows.d.ts +2 -2
  78. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  79. package/dist/native-pinned-write-windows.js +5 -5
  80. package/dist/native-pinned-write.d.ts.map +1 -1
  81. package/dist/native-pinned-write.js +14 -4
  82. package/dist/native-staged-file.d.ts +2 -2
  83. package/dist/native-staged-file.d.ts.map +1 -1
  84. package/dist/opened-file-failure.d.ts +1 -0
  85. package/dist/opened-file-failure.d.ts.map +1 -1
  86. package/dist/opened-file-failure.js +4 -0
  87. package/dist/output-sibling.d.ts.map +1 -1
  88. package/dist/output-sibling.js +9 -3
  89. package/dist/pinned-write.d.ts +2 -2
  90. package/dist/pinned-write.d.ts.map +1 -1
  91. package/dist/pinned-write.js +32 -13
  92. package/dist/publish-file.d.ts.map +1 -1
  93. package/dist/publish-file.js +7 -3
  94. package/dist/regular-file.d.ts +1 -1
  95. package/dist/regular-file.d.ts.map +1 -1
  96. package/dist/regular-file.js +35 -6
  97. package/dist/replace-file-descriptor.d.ts +9 -1
  98. package/dist/replace-file-descriptor.d.ts.map +1 -1
  99. package/dist/replace-file-descriptor.js +58 -10
  100. package/dist/replace-file-temp-owner.d.ts +7 -0
  101. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  102. package/dist/replace-file-temp-owner.js +29 -0
  103. package/dist/replace-file.d.ts +1 -0
  104. package/dist/replace-file.d.ts.map +1 -1
  105. package/dist/replace-file.js +12 -38
  106. package/dist/root-context.d.ts +4 -0
  107. package/dist/root-context.d.ts.map +1 -1
  108. package/dist/root-context.js +19 -0
  109. package/dist/root-impl.d.ts +43 -0
  110. package/dist/root-impl.d.ts.map +1 -1
  111. package/dist/root-impl.js +13 -11
  112. package/dist/root-path-symlink.d.ts +6 -2
  113. package/dist/root-path-symlink.d.ts.map +1 -1
  114. package/dist/root-path-symlink.js +7 -6
  115. package/dist/root-path.d.ts +1 -0
  116. package/dist/root-path.d.ts.map +1 -1
  117. package/dist/root-path.js +6 -2
  118. package/dist/root-walk.d.ts.map +1 -1
  119. package/dist/root-walk.js +4 -0
  120. package/dist/root-write-verification.d.ts +3 -2
  121. package/dist/root-write-verification.d.ts.map +1 -1
  122. package/dist/root-write-verification.js +6 -0
  123. package/dist/secret-file.d.ts +9 -0
  124. package/dist/secret-file.d.ts.map +1 -1
  125. package/dist/secret-file.js +136 -74
  126. package/dist/sibling-temp.d.ts.map +1 -1
  127. package/dist/sibling-temp.js +9 -3
  128. package/dist/sidecar-lock-acquire.d.ts +1 -0
  129. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  130. package/dist/sidecar-lock-acquire.js +24 -5
  131. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  132. package/dist/sidecar-lock-reclaim.js +6 -4
  133. package/dist/sidecar-lock-types.d.ts +7 -0
  134. package/dist/sidecar-lock-types.d.ts.map +1 -1
  135. package/dist/sidecar-lock.d.ts +2 -0
  136. package/dist/sidecar-lock.d.ts.map +1 -1
  137. package/dist/sidecar-lock.js +44 -3
  138. package/dist/write-open-flags.d.ts +5 -0
  139. package/dist/write-open-flags.d.ts.map +1 -0
  140. package/dist/write-open-flags.js +35 -0
  141. package/docs/advanced.md +1 -1
  142. package/docs/archive.md +56 -7
  143. package/docs/atomic.md +8 -2
  144. package/docs/contributing.md +13 -0
  145. package/docs/durability.md +3 -2
  146. package/docs/errors.md +3 -1
  147. package/docs/file-store.md +38 -8
  148. package/docs/local-roots.md +4 -3
  149. package/docs/output.md +4 -1
  150. package/docs/private-file-store.md +11 -2
  151. package/docs/reading.md +3 -1
  152. package/docs/regular-file.md +16 -2
  153. package/docs/root.md +4 -2
  154. package/docs/secret-file.md +37 -2
  155. package/docs/sidecar-lock.md +33 -5
  156. package/docs/store.md +5 -1
  157. package/docs/temp.md +19 -1
  158. package/docs/writing.md +24 -3
  159. package/package.json +14 -10
@@ -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
@@ -13,6 +13,7 @@ export type HeldSidecarLock = {
13
13
  metadata: Record<string, unknown>;
14
14
  releasePromise?: Promise<void>;
15
15
  lockRoot?: Root;
16
+ retainOnExit?: boolean;
16
17
  parsePayload?: (raw: string) => unknown;
17
18
  compromiseTimer?: NodeJS.Timeout;
18
19
  };
@@ -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,OAAO,CAAC;IACvB,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,CAqT5B"}
@@ -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 &&
@@ -43,11 +53,15 @@ export async function acquireSidecarLock(options, context) {
43
53
  held.reentrantOwner !== undefined &&
44
54
  options.reentrantOwner === held.reentrantOwner) {
45
55
  held.refCount += 1;
56
+ // Retention is monotonic: any same-owner request to keep the sidecar on
57
+ // exit upgrades the held lock; a later default acquisition never revokes it.
58
+ if (options.retainOnExit === true) {
59
+ held.retainOnExit = true;
60
+ }
46
61
  return context.handleForHeldLock(normalizedTargetPath, held);
47
62
  }
48
63
  }
49
64
  const startedAt = Date.now();
50
- const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
51
65
  const reclaimGuardPath = `${lockPath}.reclaim`;
52
66
  let ownsReclaimGuard = false;
53
67
  let attempt = 0;
@@ -59,7 +73,7 @@ export async function acquireSidecarLock(options, context) {
59
73
  if ((options.timeoutMs !== undefined &&
60
74
  options.timeoutMs !== Number.POSITIVE_INFINITY &&
61
75
  elapsed >= options.timeoutMs) ||
62
- (maxRetries !== undefined && attempt >= maxRetries)) {
76
+ (retry.retries !== undefined && attempt >= retry.retries)) {
63
77
  throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
64
78
  code: "file_lock_timeout",
65
79
  lockPath,
@@ -101,10 +115,14 @@ export async function acquireSidecarLock(options, context) {
101
115
  if (options.lockRoot) {
102
116
  const lockRoot = options.lockRoot;
103
117
  const relativeLockPath = relativeSidecarLockPath(lockRoot, lockPath);
118
+ const observation = fileObservation();
104
119
  try {
105
- await lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
120
+ await observation.run(() => lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 }));
106
121
  }
107
122
  catch (error) {
123
+ // Only this invocation's failed exclusive open grants denial retry authority.
124
+ lockFileCreateDenied = observation.has(error, `exclusive-create:${lockPath}`) &&
125
+ isTransientLockFileDenial(error, lockPath);
108
126
  if (error instanceof FsSafeError && error.code === "already-exists") {
109
127
  throw Object.assign(new Error("sidecar lock exists"), { code: "EEXIST" });
110
128
  }
@@ -157,6 +175,7 @@ export async function acquireSidecarLock(options, context) {
157
175
  acquiredAt: Date.now(),
158
176
  metadata: options.metadata ?? {},
159
177
  lockRoot: options.lockRoot,
178
+ retainOnExit: options.retainOnExit,
160
179
  parsePayload: options.parsePayload,
161
180
  };
162
181
  context.held.set(normalizedTargetPath, createdHeld);
@@ -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);
@@ -33,6 +33,13 @@ export type SidecarLockAcquireOptions<TPayload extends Record<string, unknown>>
33
33
  metadata?: Record<string, unknown>;
34
34
  parsePayload?: (raw: string) => unknown;
35
35
  lockRoot?: Root;
36
+ /**
37
+ * Keep the lock file when the process exits naturally. Default `false`:
38
+ * process-exit handlers release held locks. Set only for deliberately
39
+ * retained ownership records (for example, fail-closed build locks) whose
40
+ * liveness is governed by the caller's own stale policy.
41
+ */
42
+ retainOnExit?: boolean;
36
43
  onCompromised?: (info: SidecarLockCompromisedInfo) => void;
37
44
  compromiseCheckIntervalMs?: number;
38
45
  };
@@ -1 +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"}
1
+ {"version":3,"file":"sidecar-lock-types.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-types.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,8 @@
1
1
  import type { SidecarLockAcquireOptions, SidecarLockHandle, SidecarLockHeldEntry, WithSidecarLockOptions } from "./sidecar-lock-types.js";
2
2
  export type { SidecarLockStaleSnapshot } from "./sidecar-lock-reclaim.js";
3
3
  export type { SidecarLockAcquireOptions, SidecarLockCompromisedInfo, SidecarLockHandle, SidecarLockHeldEntry, SidecarLockRetryOptions, SidecarLockStaleRecovery, WithSidecarLockOptions, } from "./sidecar-lock-types.js";
4
+ /** True when a retain-unaware package copy registered the process-exit handlers first. */
5
+ export declare function exitCleanupCannotRetain(): boolean;
4
6
  export declare function createSidecarLockManager(key: string): {
5
7
  acquire: <TPayload extends Record<string, unknown>>(options: SidecarLockAcquireOptions<TPayload>) => Promise<SidecarLockHandle>;
6
8
  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":"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":"AAWA,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;AAqIjC,0FAA0F;AAC1F,wBAAgB,uBAAuB,IAAI,OAAO,CAMjD;AAuFD,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;eAkBL,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,4 +1,5 @@
1
1
  import fsSync from "node:fs";
2
+ import { FsSafeError } from "./errors.js";
2
3
  import { sameFileIdentity } from "./file-identity.js";
3
4
  import { removeSidecarLockIfUnchanged, sidecarLockSnapshotMatches, } from "./sidecar-lock-reclaim.js";
4
5
  import { acquireSidecarLock } from "./sidecar-lock-acquire.js";
@@ -7,6 +8,11 @@ import { createSuppressedError } from "./suppressed-error.js";
7
8
  const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
8
9
  const GLOBAL_CLEANUP_KEY = Symbol.for("fsSafe.sidecarLockCleanupRegistered");
9
10
  const GLOBAL_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.sidecarLockCleanupHandler");
11
+ const GLOBAL_BEFORE_EXIT_KEY = Symbol.for("fsSafe.sidecarLockBeforeExitCleanup");
12
+ // Set by copies whose exit handlers honor retainOnExit. When an older package
13
+ // copy registered the handlers first, this marker stays absent and retained
14
+ // acquisitions must fail closed rather than silently lose the guarantee.
15
+ const GLOBAL_RETAIN_AWARE_KEY = Symbol.for("fsSafe.sidecarLockRetainAwareCleanup");
10
16
  function getGlobalManagers() {
11
17
  const globalWithState = globalThis;
12
18
  if (!globalWithState[GLOBAL_STATE_KEY]) {
@@ -89,11 +95,12 @@ function releaseAllReclaimGuardsSync(state) {
89
95
  }
90
96
  }
91
97
  }
92
- function releaseAllLocksSync(state) {
98
+ function releaseAllLocksSync(state, options) {
93
99
  for (const [normalizedTargetPath, held] of state.held) {
94
100
  void held.handle.close().catch(() => undefined);
95
101
  try {
96
- if (!held.lockRoot && snapshotMatchesSync(held.lockPath, held.snapshot)) {
102
+ const retained = options?.preserveRetained === true && held.retainOnExit;
103
+ if (!retained && !held.lockRoot && snapshotMatchesSync(held.lockPath, held.snapshot)) {
97
104
  fsSync.rmSync(held.lockPath, { force: true });
98
105
  }
99
106
  }
@@ -109,14 +116,43 @@ function ensureGlobalExitCleanupRegistered() {
109
116
  if (globalWithCleanup[GLOBAL_CLEANUP_KEY])
110
117
  return;
111
118
  globalWithCleanup[GLOBAL_CLEANUP_KEY] = true;
119
+ globalWithCleanup[GLOBAL_RETAIN_AWARE_KEY] = true;
112
120
  const cleanup = () => {
113
121
  for (const state of getGlobalManagers().values()) {
114
- releaseAllLocksSync(state);
122
+ releaseAllLocksSync(state, { preserveRetained: true });
115
123
  }
116
124
  };
117
125
  globalWithCleanup[GLOBAL_CLEANUP_HANDLER_KEY] = cleanup;
118
126
  process.on("exit", cleanup);
119
127
  }
128
+ /** True when a retain-unaware package copy registered the process-exit handlers first. */
129
+ export function exitCleanupCannotRetain() {
130
+ const globalWithCleanup = globalThis;
131
+ return globalWithCleanup[GLOBAL_CLEANUP_KEY] === true && globalWithCleanup[GLOBAL_RETAIN_AWARE_KEY] !== true;
132
+ }
133
+ function ensureGlobalBeforeExitCleanupRegistered() {
134
+ const globalWithCleanup = globalThis;
135
+ if (globalWithCleanup[GLOBAL_BEFORE_EXIT_KEY]) {
136
+ return globalWithCleanup[GLOBAL_BEFORE_EXIT_KEY];
137
+ }
138
+ // Independent of the exit registration, which an older package copy may own.
139
+ const lifecycle = { armed: false };
140
+ globalWithCleanup[GLOBAL_BEFORE_EXIT_KEY] = lifecycle;
141
+ process.on("beforeExit", () => {
142
+ if (!lifecycle.armed)
143
+ return;
144
+ // Cleanup itself schedules I/O; retry only after another acquisition.
145
+ lifecycle.armed = false;
146
+ for (const state of getGlobalManagers().values()) {
147
+ for (const [normalizedTargetPath, held] of Array.from(state.held.entries())) {
148
+ if (held.lockRoot && !held.retainOnExit) {
149
+ void releaseHeldLock(state, normalizedTargetPath, held, { force: true }).catch(() => undefined);
150
+ }
151
+ }
152
+ }
153
+ });
154
+ return lifecycle;
155
+ }
120
156
  async function releaseHeldLock(state, normalizedTargetPath, held, options = {}) {
121
157
  const current = state.held.get(normalizedTargetPath);
122
158
  if (current !== held) {
@@ -158,6 +194,7 @@ async function releaseHeldLock(state, normalizedTargetPath, held, options = {})
158
194
  }
159
195
  }
160
196
  function handleForHeldLock(state, normalizedTargetPath, held) {
197
+ ensureGlobalBeforeExitCleanupRegistered().armed = true;
161
198
  return createHeldSidecarLockHandle({
162
199
  normalizedTargetPath,
163
200
  held,
@@ -170,8 +207,12 @@ export function createSidecarLockManager(key) {
170
207
  state.cleanupRegistered = true;
171
208
  state.reclaimCleanupRegistered = true;
172
209
  ensureGlobalExitCleanupRegistered();
210
+ ensureGlobalBeforeExitCleanupRegistered().armed = true;
173
211
  }
174
212
  async function acquire(options) {
213
+ if (options.retainOnExit === true && exitCleanupCannotRetain()) {
214
+ throw new FsSafeError("helper-unavailable", "retainOnExit requires this process's exit handlers to be retain-aware; an older package copy registered them first");
215
+ }
175
216
  return await acquireSidecarLock(options, {
176
217
  held: state.held,
177
218
  reclaimGuards: state.reclaimGuards,
@@ -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"}