@openclaw/fs-safe 0.15.0 → 0.17.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 (311) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +36 -7
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +2 -8
  5. package/dist/advanced.d.ts +3 -0
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +3 -0
  8. package/dist/archive-deadline.d.ts.map +1 -1
  9. package/dist/archive-deadline.js +22 -23
  10. package/dist/archive-kind.d.ts +0 -1
  11. package/dist/archive-kind.d.ts.map +1 -1
  12. package/dist/archive-kind.js +5 -17
  13. package/dist/archive-merge.d.ts +1 -0
  14. package/dist/archive-merge.d.ts.map +1 -1
  15. package/dist/archive-merge.js +4 -4
  16. package/dist/archive-native.d.ts +1 -0
  17. package/dist/archive-native.d.ts.map +1 -1
  18. package/dist/archive-native.js +1 -0
  19. package/dist/archive-options.d.ts +2 -0
  20. package/dist/archive-options.d.ts.map +1 -1
  21. package/dist/archive-parser.wasm +0 -0
  22. package/dist/archive-read.d.ts.map +1 -1
  23. package/dist/archive-read.js +6 -7
  24. package/dist/archive-tar-stream.d.ts +3 -0
  25. package/dist/archive-tar-stream.d.ts.map +1 -1
  26. package/dist/archive-tar-stream.js +56 -37
  27. package/dist/archive-tar-wasm.d.ts +16 -4
  28. package/dist/archive-tar-wasm.d.ts.map +1 -1
  29. package/dist/archive-tar-wasm.js +134 -34
  30. package/dist/archive-zip-count.d.ts.map +1 -1
  31. package/dist/archive-zip-count.js +21 -1
  32. package/dist/archive-zip-directory.d.ts.map +1 -1
  33. package/dist/archive-zip-directory.js +23 -1
  34. package/dist/archive-zip-loader.d.ts +2 -0
  35. package/dist/archive-zip-loader.d.ts.map +1 -1
  36. package/dist/archive-zip-loader.js +7 -0
  37. package/dist/archive-zip-names.d.ts.map +1 -1
  38. package/dist/archive-zip-names.js +7 -2
  39. package/dist/archive.d.ts.map +1 -1
  40. package/dist/archive.js +14 -9
  41. package/dist/byte-view.d.ts +3 -0
  42. package/dist/byte-view.d.ts.map +1 -0
  43. package/dist/byte-view.js +13 -0
  44. package/dist/clone-metadata.d.ts +1 -0
  45. package/dist/clone-metadata.d.ts.map +1 -1
  46. package/dist/clone-metadata.js +6 -2
  47. package/dist/create-directory.d.ts +20 -0
  48. package/dist/create-directory.d.ts.map +1 -0
  49. package/dist/create-directory.js +130 -0
  50. package/dist/create-file-async.d.ts +7 -0
  51. package/dist/create-file-async.d.ts.map +1 -0
  52. package/dist/create-file-async.js +121 -0
  53. package/dist/create-file.d.ts +8 -0
  54. package/dist/create-file.d.ts.map +1 -0
  55. package/dist/create-file.js +190 -0
  56. package/dist/create-owned-file.d.ts +8 -0
  57. package/dist/create-owned-file.d.ts.map +1 -0
  58. package/dist/create-owned-file.js +16 -0
  59. package/dist/create.d.ts +4 -0
  60. package/dist/create.d.ts.map +1 -0
  61. package/dist/create.js +2 -0
  62. package/dist/creation-darwin.d.ts +6 -0
  63. package/dist/creation-darwin.d.ts.map +1 -0
  64. package/dist/creation-darwin.js +70 -0
  65. package/dist/creation-file-state.d.ts +19 -0
  66. package/dist/creation-file-state.d.ts.map +1 -0
  67. package/dist/creation-file-state.js +118 -0
  68. package/dist/creation-path.d.ts +21 -0
  69. package/dist/creation-path.d.ts.map +1 -0
  70. package/dist/creation-path.js +71 -0
  71. package/dist/creation-permissions.d.ts +19 -0
  72. package/dist/creation-permissions.d.ts.map +1 -0
  73. package/dist/creation-permissions.js +125 -0
  74. package/dist/directory-durability.d.ts +7 -7
  75. package/dist/directory-durability.d.ts.map +1 -1
  76. package/dist/directory-durability.js +22 -80
  77. package/dist/directory-guard.d.ts +3 -0
  78. package/dist/directory-guard.d.ts.map +1 -1
  79. package/dist/directory-mode-node.d.ts +2 -0
  80. package/dist/directory-mode-node.d.ts.map +1 -1
  81. package/dist/directory-mode-node.js +8 -0
  82. package/dist/directory-receipt.d.ts +24 -0
  83. package/dist/directory-receipt.d.ts.map +1 -0
  84. package/dist/directory-receipt.js +123 -0
  85. package/dist/file-cleanup.d.ts +20 -0
  86. package/dist/file-cleanup.d.ts.map +1 -0
  87. package/dist/file-cleanup.js +81 -0
  88. package/dist/file-contents.d.ts +6 -0
  89. package/dist/file-contents.d.ts.map +1 -0
  90. package/dist/file-contents.js +40 -0
  91. package/dist/file-hash.d.ts.map +1 -1
  92. package/dist/file-hash.js +16 -4
  93. package/dist/file-lock-sync-root-acquire.d.ts.map +1 -1
  94. package/dist/file-lock-sync-root-acquire.js +3 -0
  95. package/dist/file-lock-sync-root-held.d.ts +1 -2
  96. package/dist/file-lock-sync-root-held.d.ts.map +1 -1
  97. package/dist/file-lock-sync-root-held.js +7 -5
  98. package/dist/file-lock-sync-stale-admission.d.ts.map +1 -1
  99. package/dist/file-lock-sync-stale-admission.js +3 -0
  100. package/dist/file-lock-sync.d.ts.map +1 -1
  101. package/dist/file-lock-sync.js +8 -11
  102. package/dist/file-observation.d.ts +1 -1
  103. package/dist/file-observation.d.ts.map +1 -1
  104. package/dist/file-store-boundary.d.ts +2 -6
  105. package/dist/file-store-boundary.d.ts.map +1 -1
  106. package/dist/file-store-boundary.js +3 -9
  107. package/dist/file-store-sync-write.d.ts.map +1 -1
  108. package/dist/file-store-sync-write.js +2 -5
  109. package/dist/file-store.js +3 -3
  110. package/dist/guarded-mkdir.d.ts +1 -0
  111. package/dist/guarded-mkdir.d.ts.map +1 -1
  112. package/dist/guarded-mkdir.js +27 -19
  113. package/dist/install-path.d.ts.map +1 -1
  114. package/dist/install-path.js +2 -5
  115. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  116. package/dist/json-durable-queue-ownership.js +2 -6
  117. package/dist/json-durable-queue-paths.d.ts.map +1 -1
  118. package/dist/json-durable-queue-paths.js +2 -24
  119. package/dist/json-durable-queue.d.ts.map +1 -1
  120. package/dist/json-durable-queue.js +10 -9
  121. package/dist/json.d.ts.map +1 -1
  122. package/dist/json.js +32 -75
  123. package/dist/local-roots.d.ts.map +1 -1
  124. package/dist/local-roots.js +19 -21
  125. package/dist/move-path-cleanup.d.ts +5 -19
  126. package/dist/move-path-cleanup.d.ts.map +1 -1
  127. package/dist/move-path-cleanup.js +57 -21
  128. package/dist/move-path.d.ts.map +1 -1
  129. package/dist/move-path.js +63 -40
  130. package/dist/native-binding.d.ts +11 -1
  131. package/dist/native-binding.d.ts.map +1 -1
  132. package/dist/native-fallback-warning.d.ts +4 -0
  133. package/dist/native-fallback-warning.d.ts.map +1 -0
  134. package/dist/native-fallback-warning.js +11 -0
  135. package/dist/native-operations.d.ts +0 -2
  136. package/dist/native-operations.d.ts.map +1 -1
  137. package/dist/native-operations.js +0 -24
  138. package/dist/native-parent-admission.d.ts +2 -0
  139. package/dist/native-parent-admission.d.ts.map +1 -1
  140. package/dist/native-parent-admission.js +3 -2
  141. package/dist/native-pinned-write-windows.d.ts +1 -1
  142. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  143. package/dist/native-pinned-write-windows.js +173 -28
  144. package/dist/native-pinned-write.d.ts.map +1 -1
  145. package/dist/native-pinned-write.js +19 -3
  146. package/dist/native-policy-parent-windows.d.ts.map +1 -1
  147. package/dist/native-policy-parent-windows.js +15 -6
  148. package/dist/native-staged-file.d.ts +5 -3
  149. package/dist/native-staged-file.d.ts.map +1 -1
  150. package/dist/native-staged-file.js +90 -40
  151. package/dist/native.js +2 -2
  152. package/dist/opened-realpath.d.ts.map +1 -1
  153. package/dist/opened-realpath.js +11 -2
  154. package/dist/owner-dacl.d.ts.map +1 -1
  155. package/dist/owner-dacl.js +10 -4
  156. package/dist/path.d.ts.map +1 -1
  157. package/dist/path.js +2 -1
  158. package/dist/permissions.d.ts.map +1 -1
  159. package/dist/permissions.js +3 -17
  160. package/dist/pinned-write-input.d.ts +4 -0
  161. package/dist/pinned-write-input.d.ts.map +1 -0
  162. package/dist/pinned-write-input.js +35 -0
  163. package/dist/pinned-write-mode.d.ts +5 -0
  164. package/dist/pinned-write-mode.d.ts.map +1 -0
  165. package/dist/pinned-write-mode.js +31 -0
  166. package/dist/pinned-write-staged.d.ts +6 -0
  167. package/dist/pinned-write-staged.d.ts.map +1 -0
  168. package/dist/pinned-write-staged.js +186 -0
  169. package/dist/pinned-write-types.d.ts +3 -0
  170. package/dist/pinned-write-types.d.ts.map +1 -1
  171. package/dist/pinned-write.d.ts.map +1 -1
  172. package/dist/pinned-write.js +41 -147
  173. package/dist/private-directory.d.ts.map +1 -1
  174. package/dist/private-directory.js +18 -4
  175. package/dist/private-producer-handoff-sync.d.ts +14 -0
  176. package/dist/private-producer-handoff-sync.d.ts.map +1 -0
  177. package/dist/private-producer-handoff-sync.js +114 -0
  178. package/dist/private-producer-handoff.d.ts +22 -4
  179. package/dist/private-producer-handoff.d.ts.map +1 -1
  180. package/dist/private-producer-handoff.js +140 -77
  181. package/dist/publish-copy-stage.d.ts +2 -1
  182. package/dist/publish-copy-stage.d.ts.map +1 -1
  183. package/dist/publish-copy-stage.js +16 -7
  184. package/dist/publish-file.d.ts +2 -2
  185. package/dist/publish-file.d.ts.map +1 -1
  186. package/dist/publish-file.js +58 -98
  187. package/dist/regular-file.d.ts.map +1 -1
  188. package/dist/regular-file.js +35 -44
  189. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  190. package/dist/replace-file-copy-fallback.js +28 -26
  191. package/dist/replace-file-copy-source.d.ts.map +1 -1
  192. package/dist/replace-file-copy-source.js +13 -22
  193. package/dist/replace-file-descriptor.d.ts.map +1 -1
  194. package/dist/replace-file-descriptor.js +10 -16
  195. package/dist/replace-file-temp-owner.d.ts +0 -7
  196. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  197. package/dist/replace-file-temp-owner.js +9 -60
  198. package/dist/replace-file.d.ts.map +1 -1
  199. package/dist/replace-file.js +9 -13
  200. package/dist/root-create-input.d.ts +2 -1
  201. package/dist/root-create-input.d.ts.map +1 -1
  202. package/dist/root-create-input.js +13 -4
  203. package/dist/root-directory-creation.d.ts +3 -3
  204. package/dist/root-directory-creation.d.ts.map +1 -1
  205. package/dist/root-directory-creation.js +15 -3
  206. package/dist/root-directory-list.d.ts.map +1 -1
  207. package/dist/root-directory-list.js +20 -3
  208. package/dist/root-file-final-admission.d.ts +1 -1
  209. package/dist/root-file-final-admission.d.ts.map +1 -1
  210. package/dist/root-file-final-admission.js +5 -2
  211. package/dist/root-file.d.ts.map +1 -1
  212. package/dist/root-file.js +3 -2
  213. package/dist/root-impl.d.ts.map +1 -1
  214. package/dist/root-impl.js +78 -27
  215. package/dist/root-move-noreplace.d.ts +2 -0
  216. package/dist/root-move-noreplace.d.ts.map +1 -1
  217. package/dist/root-move-noreplace.js +22 -13
  218. package/dist/root-options.d.ts +12 -4
  219. package/dist/root-options.d.ts.map +1 -1
  220. package/dist/root-path-stat.d.ts.map +1 -1
  221. package/dist/root-path-stat.js +59 -7
  222. package/dist/root-read-admission.d.ts.map +1 -1
  223. package/dist/root-read-admission.js +7 -2
  224. package/dist/root-remove.d.ts.map +1 -1
  225. package/dist/root-remove.js +15 -1
  226. package/dist/root-write-publication.js +1 -1
  227. package/dist/secret-file.d.ts.map +1 -1
  228. package/dist/secret-file.js +1 -0
  229. package/dist/secure-file-windows.d.ts +6 -0
  230. package/dist/secure-file-windows.d.ts.map +1 -1
  231. package/dist/secure-file-windows.js +34 -117
  232. package/dist/secure-file.js +2 -2
  233. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  234. package/dist/sidecar-lock-acquire.js +4 -6
  235. package/dist/sidecar-lock-handle.d.ts +3 -0
  236. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  237. package/dist/sidecar-lock-handle.js +6 -0
  238. package/dist/sidecar-lock-reclaim.d.ts +1 -1
  239. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  240. package/dist/sidecar-lock-reclaim.js +11 -8
  241. package/dist/sidecar-lock-root.d.ts.map +1 -1
  242. package/dist/sidecar-lock-root.js +2 -1
  243. package/dist/sidecar-lock.d.ts.map +1 -1
  244. package/dist/sidecar-lock.js +3 -5
  245. package/dist/staged-directory.d.ts +2 -2
  246. package/dist/staged-directory.d.ts.map +1 -1
  247. package/dist/staged-directory.js +6 -6
  248. package/dist/staged-file-settlement.d.ts +17 -0
  249. package/dist/staged-file-settlement.d.ts.map +1 -0
  250. package/dist/staged-file-settlement.js +57 -0
  251. package/dist/strict-file-identity.d.ts +1 -1
  252. package/dist/strict-file-identity.d.ts.map +1 -1
  253. package/dist/strict-file-identity.js +9 -9
  254. package/dist/symlink-parents.d.ts.map +1 -1
  255. package/dist/symlink-parents.js +2 -27
  256. package/dist/temp-workspace-owner.js +4 -4
  257. package/dist/unicode-path.d.ts.map +1 -1
  258. package/dist/unicode-path.js +3 -0
  259. package/dist/walk.d.ts.map +1 -1
  260. package/dist/walk.js +4 -2
  261. package/dist/windows-owner.d.ts.map +1 -1
  262. package/dist/windows-owner.js +2 -1
  263. package/dist/windows-security-bridge.cs +336 -0
  264. package/dist/windows-security-bridge.ps1 +15 -0
  265. package/dist/windows-security-command.d.ts +26 -0
  266. package/dist/windows-security-command.d.ts.map +1 -0
  267. package/dist/windows-security-command.js +363 -0
  268. package/dist/windows-security-facts.d.ts +6 -0
  269. package/dist/windows-security-facts.d.ts.map +1 -0
  270. package/dist/windows-security-facts.js +108 -0
  271. package/dist/write-file-handle.d.ts +7 -0
  272. package/dist/write-file-handle.d.ts.map +1 -1
  273. package/dist/write-file-handle.js +23 -0
  274. package/dist/write-open-flags.d.ts.map +1 -1
  275. package/dist/write-open-flags.js +1 -8
  276. package/dist/write-queue.d.ts.map +1 -1
  277. package/dist/write-queue.js +1 -4
  278. package/docs/advanced.md +71 -2
  279. package/docs/archive.md +102 -39
  280. package/docs/atomic.md +29 -5
  281. package/docs/config.md +6 -2
  282. package/docs/contributing.md +48 -4
  283. package/docs/copy.md +2 -0
  284. package/docs/creation.md +132 -0
  285. package/docs/durability.md +59 -0
  286. package/docs/file-contents.md +68 -0
  287. package/docs/install.md +31 -7
  288. package/docs/json.md +5 -4
  289. package/docs/local-roots.md +2 -0
  290. package/docs/migrating-to-0.5.md +15 -6
  291. package/docs/migrating-to-0.6.md +9 -4
  292. package/docs/mutation-policy-proof.md +5 -3
  293. package/docs/native-helper.md +22 -9
  294. package/docs/native.md +47 -15
  295. package/docs/path.md +4 -4
  296. package/docs/permissions.md +37 -14
  297. package/docs/public-api.md +5 -0
  298. package/docs/quickstart.md +1 -1
  299. package/docs/reading.md +2 -2
  300. package/docs/regular-file.md +3 -0
  301. package/docs/root.md +43 -0
  302. package/docs/secret-file.md +11 -2
  303. package/docs/secure-file.md +9 -4
  304. package/docs/sidecar-lock.md +14 -5
  305. package/docs/staged-file.md +9 -3
  306. package/docs/store.md +3 -1
  307. package/docs/temp.md +4 -1
  308. package/docs/types.md +18 -2
  309. package/docs/walk.md +7 -0
  310. package/docs/writing.md +80 -7
  311. package/package.json +18 -15
package/dist/root-impl.js CHANGED
@@ -56,6 +56,7 @@ import fs from "node:fs/promises";
56
56
  import path from "node:path";
57
57
  import { normalizeMaxBytes } from "./byte-budget.js";
58
58
  import { assertCopySourceCurrent, resolveFileCopyCloneMode } from "./copy-file-input.js";
59
+ import { assertPrivateFileCreationAvailable, resolveCreationPermissions } from "./creation-permissions.js";
59
60
  import { assertAsyncDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
60
61
  import { FsSafeError } from "./errors.js";
61
62
  import { syncDirectoryBestEffort } from "./directory-durability.js";
@@ -71,18 +72,19 @@ import { validatePinnedOperationPayload } from "./pinned-operation.js";
71
72
  import { PATH_ALIAS_POLICIES } from "./path-policy.js";
72
73
  import { assertNoNulPathInput, hasNodeErrorCode, isNotFoundPathError, isSymlinkOpenError, } from "./path.js";
73
74
  import { readOpenedFileSafely } from "./read-opened-file.js";
74
- import { cleanupPinnedFilePath, removePathIfIdentityUnchanged } from "./replace-file-temp-owner.js";
75
+ import { cleanupPinnedFilePath } from "./file-cleanup.js";
76
+ import { removePathIfIdentityUnchanged } from "./replace-file-temp-owner.js";
75
77
  import { realpathSync } from "./realpath.js";
76
78
  import { mkdirPathFallback, prepareRootWriteTarget, tryMkdirAtExactParent } from "./root-directory-creation.js";
77
79
  import { isNonRegularWriteOpenError, resolveNonblockingWriteFlag } from "./write-open-flags.js";
78
- import { resolveRootPath, resolveRootPathForRemoval } from "./root-path.js";
80
+ import { resolveRootPath, resolveRootPathSync, resolveRootPathForRemoval } from "./root-path.js";
79
81
  import { RemovalPathReceipts } from "./root-remove-receipt.js";
80
82
  import { admitPathInsideRoot } from "./root-boundary.js";
81
83
  import { listDirectoryPath, openRootDirectoryListing } from "./root-directory-list.js";
82
84
  import { statResolvedPathInRoot } from "./root-path-stat.js";
83
85
  import { entriesInRoot } from "./root-entries.js";
84
86
  import { assertMoveMutationAllowed } from "./root-move-preflight.js";
85
- import { assertRootIdentityCurrent, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
87
+ import { assertRootIdentityCurrent, assertRootIdentityCurrentSync, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
86
88
  import { fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, outsideWorkspaceError, } from "./root-errors.js";
87
89
  import { getFsSafeTestHooks } from "./test-hooks.js";
88
90
  import { stringifyJsonDocument } from "./json-stringify.js";
@@ -97,7 +99,7 @@ import { finishRootFallbackWrite } from "./root-write-publication.js";
97
99
  import { withRootFallbackCompatibilityLock } from "./root-write-compatibility.js";
98
100
  import { assertRootFallbackWritePath } from "./root-write-lock-binding.js";
99
101
  import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
100
- import { movePathNoReplaceNative } from "./root-move-noreplace.js";
102
+ import { admitMoveSourceStat, movePathNoReplaceNative } from "./root-move-noreplace.js";
101
103
  import { admitRootReadHandle, inspectOpenedPathIdentitySync } from "./root-read-admission.js";
102
104
  import { createCopyPublicationObserver, onCopyPublication, onCopySourceAdmission } from "./copy-publication.js";
103
105
  import { writeAllToFile } from "./write-file-handle.js";
@@ -315,13 +317,15 @@ export class RootHandle {
315
317
  }
316
318
  async create(relativePath, data, options = {}) {
317
319
  assertValidRootDestinationPath(relativePath);
320
+ const durable = options.durable ?? this.defaults.durable ?? true;
318
321
  await writeFileInRoot(this.context, {
319
322
  relativePath,
320
323
  data,
321
324
  mkdir: this.defaults.mkdir,
322
325
  mode: this.defaults.mode,
323
326
  ...this.mutationOptions(createInputOptions(data, options, this.defaults.maxBytes)),
324
- durable: options.durable ?? this.defaults.durable ?? true,
327
+ durable: durable !== false,
328
+ strictFileSync: durable === "file",
325
329
  overwrite: false,
326
330
  }).catch(rethrowMutationAuthorityError).catch(rethrowCreateInputError);
327
331
  }
@@ -377,7 +381,9 @@ export class RootHandle {
377
381
  assertValidRootRelativePath(fromRelative);
378
382
  assertValidRootDestinationPath(toRelative);
379
383
  validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
380
- const { denyMutations, assertBeforeMutation, mutationSymlinks } = this.mutationOptions(options);
384
+ const mutationOptions = this.mutationOptions(options);
385
+ const { assertBeforeMutation } = mutationOptions;
386
+ const { denyMutations, mutationSymlinks } = snapshotPinnedMutationPolicy(mutationOptions.denyMutations, mutationOptions.mutationSymlinks) ?? {};
381
387
  const overwrite = options.overwrite ?? false;
382
388
  await assertMoveMutationAllowed(this.context, {
383
389
  fromRelative,
@@ -717,8 +723,9 @@ async function appendFileInRoot(root, params) {
717
723
  if (params.prependNewlineIfNeeded === true &&
718
724
  !target.createdForWrite &&
719
725
  target.stat.size > 0 &&
726
+ params.data.length > 0 &&
720
727
  ((typeof params.data === "string" && !params.data.startsWith("\n")) ||
721
- (Buffer.isBuffer(params.data) && params.data.length > 0 && params.data[0] !== 0x0a))) {
728
+ (Buffer.isBuffer(params.data) && params.data[0] !== 0x0a))) {
722
729
  const newline = Buffer.from("\n", typeof params.data === "string" ? params.encoding : "utf8");
723
730
  const tail = Buffer.alloc(newline.length);
724
731
  const { bytesRead } = await target.handle.read(tail, 0, tail.length, Math.max(0, target.stat.size - tail.length));
@@ -778,6 +785,7 @@ async function removePathInRoot(root, params) {
778
785
  }
779
786
  }
780
787
  async function mkdirPathInRoot(root, params) {
788
+ const privateMode = resolveCreationPermissions(params, true).private;
781
789
  validatePinnedOperationPayload({ relativePath: params.relativePath });
782
790
  const policy = params.denyMutations === undefined && params.mutationSymlinks === undefined
783
791
  ? undefined
@@ -810,19 +818,23 @@ async function mkdirPathInRoot(root, params) {
810
818
  await getFsSafeTestHooks()?.beforePinnedWriteParentAdmission?.(resolved.resolved);
811
819
  }
812
820
  try {
813
- if (prepared?.mutationAdmission && params.assertBeforeMutation === undefined &&
821
+ if (!privateMode && prepared?.mutationAdmission && params.assertBeforeMutation === undefined &&
814
822
  await tryMkdirAtExactParent(root, resolved.resolved, prepared.mutationAdmission))
815
823
  return;
816
- await mkdirPathFallback(root, resolved, params.assertBeforeMutation, policy?.mutationSymlinks !== undefined, prepared?.mutationAdmission);
824
+ await mkdirPathFallback(root, resolved, params.assertBeforeMutation, policy?.mutationSymlinks !== undefined, prepared?.mutationAdmission, privateMode);
817
825
  }
818
826
  catch (error) {
819
827
  throw normalizePinnedPathError(error);
820
828
  }
821
829
  }
822
830
  async function writeFileInRoot(root, params) {
831
+ if (params.private !== undefined) {
832
+ const permissions = resolveCreationPermissions(params, false);
833
+ params = { ...params, private: permissions.private, mode: permissions.mode };
834
+ }
823
835
  const input = rootWriteInput(params);
824
836
  await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
825
- if (input.kind === "buffer" && process.platform === "win32" &&
837
+ if (!params.private && input.kind === "buffer" && !input.stageBeforePublish && process.platform === "win32" &&
826
838
  (params.renameIdentity === "verify-content-with-lock" || !getNativeBinding())) {
827
839
  await writeFileFallback(root, { ...params, data: input.data });
828
840
  return;
@@ -836,15 +848,25 @@ async function writeFileInRoot(root, params) {
836
848
  async function commitPinnedWriteInRoot(root, pinned, params, input) {
837
849
  let verifyingPublication = false;
838
850
  try {
851
+ if (params.private)
852
+ assertPrivateFileCreationAvailable();
853
+ if (params.private && params.mkdir !== false) {
854
+ const target = await prepareRootWriteTarget(root, pinned.targetPath, params.assertBeforeMutation, pinned.mutationAdmission, true);
855
+ if (target !== pinned.targetPath) {
856
+ throw new FsSafeError("path-mismatch", "private creation parent changed during admission");
857
+ }
858
+ }
839
859
  await runPinnedWriteWithRenamePolicy({
840
860
  rootPath: pinned.rootReal,
841
861
  relativeParentPath: pinned.relativeParentPath,
842
862
  basename: pinned.basename,
843
863
  targetPath: pinned.targetPath,
844
864
  renameIdentity: params.renameIdentity,
845
- mkdir: params.mkdir !== false,
865
+ mkdir: !params.private && params.mkdir !== false,
866
+ private: params.private,
846
867
  mode: params.mode ?? pinned.mode,
847
868
  sync: params.durable !== false,
869
+ strictFileSync: params.strictFileSync,
848
870
  overwrite: params.overwrite,
849
871
  rejectFinalSymlink: params.mutationSymlinks !== undefined,
850
872
  input,
@@ -1064,6 +1086,8 @@ async function listPathFallback(root, relativePath, withFileTypes) {
1064
1086
  return await listDirectoryPath(root, resolved.resolved, withFileTypes, resolved.receipt);
1065
1087
  }
1066
1088
  async function movePathFallback(root, params) {
1089
+ const originalRoutes = params.overwrite && params.assertBeforeMutation
1090
+ ? await Promise.all([params.fromRelative, params.toRelative].map(expandRelativePathWithHome)) : undefined;
1067
1091
  const source = await resolvePathInRoot(root, params.fromRelative, {
1068
1092
  aliasErrorCode: "path-alias",
1069
1093
  allowFinalSymlink: true,
@@ -1099,9 +1123,14 @@ async function movePathFallback(root, params) {
1099
1123
  targetStat?.isSymbolicLink() === true);
1100
1124
  },
1101
1125
  });
1102
- let sourceStat;
1126
+ let sourceIdentity;
1103
1127
  try {
1104
- sourceStat = fsSync.lstatSync(source.resolved);
1128
+ if (params.overwrite && params.assertBeforeMutation) {
1129
+ sourceIdentity = inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(source.resolved, { bigint: true }), true));
1130
+ }
1131
+ else {
1132
+ admitMoveSourceStat(fsSync.lstatSync(source.resolved), params.overwrite);
1133
+ }
1105
1134
  }
1106
1135
  catch (error) {
1107
1136
  if (isNotFoundPathError(error)) {
@@ -1109,19 +1138,10 @@ async function movePathFallback(root, params) {
1109
1138
  }
1110
1139
  throw error;
1111
1140
  }
1112
- if (sourceStat.isSymbolicLink()) {
1113
- throw new FsSafeError("symlink", "symlink not allowed");
1114
- }
1115
- if (sourceStat.isFile() && sourceStat.nlink > 1) {
1116
- throw hardlinkedPathNotAllowedError();
1117
- }
1118
- if (!params.overwrite && sourceStat.isDirectory()) {
1119
- throw new FsSafeError("invalid-path", "directory moves require overwrite: true");
1141
+ if (!pinnedTarget) {
1142
+ throw new FsSafeError("path-mismatch", "destination admission was not completed");
1120
1143
  }
1121
1144
  if (!params.overwrite) {
1122
- if (!pinnedTarget) {
1123
- throw new FsSafeError("path-mismatch", "destination admission was not completed");
1124
- }
1125
1145
  await movePathNoReplaceNative(root, params, {
1126
1146
  sourcePath: source.resolved,
1127
1147
  sourceParentPath: path.dirname(pinnedSource.canonicalPath),
@@ -1130,8 +1150,9 @@ async function movePathFallback(root, params) {
1130
1150
  });
1131
1151
  return;
1132
1152
  }
1133
- const sourceParentGuard = await createAsyncDirectoryGuard(path.dirname(source.resolved));
1134
- const targetParentGuard = await createNearestExistingDirectoryGuard(target.rootReal, path.dirname(target.resolved));
1153
+ const guardOptions = { bigint: sourceIdentity !== undefined };
1154
+ const sourceParentGuard = await createAsyncDirectoryGuard(path.dirname(source.resolved), guardOptions);
1155
+ const targetParentGuard = await createNearestExistingDirectoryGuard(target.rootReal, path.dirname(target.resolved), guardOptions);
1135
1156
  await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("move", target.resolved);
1136
1157
  await assertAsyncDirectoryGuard(sourceParentGuard);
1137
1158
  await assertAsyncDirectoryGuard(targetParentGuard);
@@ -1139,6 +1160,31 @@ async function movePathFallback(root, params) {
1139
1160
  assertFinalSymlinkRejected(source.resolved, params.mutationSymlinks !== undefined);
1140
1161
  assertFinalSymlinkRejected(target.resolved, params.mutationSymlinks !== undefined);
1141
1162
  params.assertBeforeMutation?.();
1163
+ if (sourceIdentity) {
1164
+ assertRootIdentityCurrentSync(root);
1165
+ assertSyncDirectoryGuard(sourceParentGuard);
1166
+ assertSyncDirectoryGuard(targetParentGuard);
1167
+ for (const [route, selected, expected, policy] of [
1168
+ [originalRoutes[0], source.resolved, pinnedSource.canonicalPath, PATH_ALIAS_POLICIES.strict],
1169
+ [originalRoutes[1], target.resolved, pinnedTarget.canonicalPath, PATH_ALIAS_POLICIES.unlinkTarget],
1170
+ ]) {
1171
+ for (const absolutePath of new Set([path.isAbsolute(route) ? route : `${root.rootWithSep}${route}`, selected])) {
1172
+ try {
1173
+ const current = resolveRootPathSync({
1174
+ absolutePath, rootPath: root.rootReal, rootCanonicalPath: root.rootReal,
1175
+ rootIdentity: root.rootIdentity, boundaryLabel: "root", policy,
1176
+ ...mutationSymlinkResolution(params.mutationSymlinks),
1177
+ });
1178
+ if (current.canonicalPath !== expected)
1179
+ throw new FsSafeError("path-mismatch", "move route changed during authorization");
1180
+ }
1181
+ catch (error) {
1182
+ throw normalizePinnedPathError(error);
1183
+ }
1184
+ }
1185
+ }
1186
+ inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(source.resolved, { bigint: true }), true), sourceIdentity);
1187
+ }
1142
1188
  await fs.rename(source.resolved, target.resolved);
1143
1189
  }
1144
1190
  catch (error) {
@@ -1303,6 +1349,7 @@ async function writeMissingFileFallback(root, params, expectedWritePath) {
1303
1349
  const parentGuard = pathSelection?.parentGuard ??
1304
1350
  await createAsyncDirectoryGuard(path.dirname(targetPath), { bigint: true });
1305
1351
  let created = false;
1352
+ let completed = false;
1306
1353
  let createdIdentity;
1307
1354
  let writtenHandle;
1308
1355
  let verifyingPublication = false;
@@ -1346,6 +1393,7 @@ async function writeMissingFileFallback(root, params, expectedWritePath) {
1346
1393
  });
1347
1394
  if (params.durable !== false)
1348
1395
  await syncDirectoryBestEffort(path.dirname(targetPath));
1396
+ completed = true;
1349
1397
  }
1350
1398
  catch (err) {
1351
1399
  if (verifyingPublication)
@@ -1363,6 +1411,9 @@ async function writeMissingFileFallback(root, params, expectedWritePath) {
1363
1411
  pathname: targetPath, handle: writtenHandle, identity: createdIdentity, parentGuard,
1364
1412
  });
1365
1413
  }
1366
- await writtenHandle?.close().catch(() => undefined);
1414
+ if (completed)
1415
+ await writtenHandle?.close();
1416
+ else
1417
+ await writtenHandle?.close().catch(() => undefined);
1367
1418
  }
1368
1419
  }
@@ -1,6 +1,8 @@
1
+ import { type BigIntStats, type Stats } from "node:fs";
1
2
  import { type DenyMutationPolicy } from "./deny-mutations.js";
2
3
  import { type RootContext } from "./root-context.js";
3
4
  import { type MutationSymlinkPolicy } from "./root-symlink-policy.js";
5
+ export declare function admitMoveSourceStat<T extends Stats | BigIntStats>(stat: T, overwrite?: boolean): T;
4
6
  export declare function movePathNoReplaceNative(root: RootContext, params: {
5
7
  assertBeforeMutation?: () => void;
6
8
  denyMutations?: DenyMutationPolicy;
@@ -1 +1 @@
1
- {"version":3,"file":"root-move-noreplace.d.ts","sourceRoot":"","sources":["../src/root-move-noreplace.ts"],"names":[],"mappings":"AAIA,OAAO,EAA2B,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAQvF,OAAO,EAA4D,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAU/G,OAAO,EAA8B,KAAK,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAqDlG,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE;IACN,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;CAC1C,EACD,KAAK,EAAE;IACL,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;CAC1B,GACA,OAAO,CAAC,IAAI,CAAC,CA0If"}
1
+ {"version":3,"file":"root-move-noreplace.d.ts","sourceRoot":"","sources":["../src/root-move-noreplace.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAI/D,OAAO,EAA2B,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAQvF,OAAO,EAA4D,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAU/G,OAAO,EAA8B,KAAK,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAsDlG,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,KAAK,GAAG,WAAW,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,UAAQ,GAAG,CAAC,CAWhG;AAED,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE;IACN,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;CAC1C,EACD,KAAK,EAAE;IACL,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;CAC1B,GACA,OAAO,CAAC,IAAI,CAAC,CAsIf"}
@@ -12,6 +12,7 @@ import { admitPathInsideRoot } from "./root-boundary.js";
12
12
  import { fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, outsideWorkspaceError, } from "./root-errors.js";
13
13
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
14
14
  import { createSuppressedError } from "./suppressed-error.js";
15
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
15
16
  import { getFsSafeTestHooks } from "./test-hooks.js";
16
17
  function nativeParentRelativePath(rootReal, parentPath) {
17
18
  const relative = path.relative(rootReal, parentPath);
@@ -57,6 +58,18 @@ function normalizeRenameNoReplaceError(error) {
57
58
  }
58
59
  return normalizeMoveError(error);
59
60
  }
61
+ export function admitMoveSourceStat(stat, overwrite = false) {
62
+ if (stat.isSymbolicLink()) {
63
+ throw new FsSafeError("symlink", "symlink not allowed");
64
+ }
65
+ if (stat.isFile() && stat.nlink > 1) {
66
+ throw hardlinkedPathNotAllowedError();
67
+ }
68
+ if (!overwrite && stat.isDirectory()) {
69
+ throw new FsSafeError("invalid-path", "directory moves require overwrite: true");
70
+ }
71
+ return stat;
72
+ }
60
73
  export async function movePathNoReplaceNative(root, params, paths) {
61
74
  try {
62
75
  fsSync.lstatSync(paths.targetPath);
@@ -104,21 +117,13 @@ export async function movePathNoReplaceNative(root, params, paths) {
104
117
  await assertRootIdentityCurrent(root);
105
118
  for (const admission of parentAdmissions)
106
119
  assertSyncDirectoryGuard(admission.guard);
120
+ // Capture an exact receipt only when admission must survive a callback.
107
121
  let sourceStat;
108
- try {
109
- sourceStat = fsSync.lstatSync(admittedSourcePath);
122
+ if (params.assertBeforeMutation) {
123
+ sourceStat = inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(admittedSourcePath, { bigint: true })));
110
124
  }
111
- catch (error) {
112
- throw normalizeMoveError(error);
113
- }
114
- if (sourceStat.isSymbolicLink()) {
115
- throw new FsSafeError("symlink", "symlink not allowed");
116
- }
117
- if (sourceStat.isFile() && sourceStat.nlink > 1) {
118
- throw hardlinkedPathNotAllowedError();
119
- }
120
- if (sourceStat.isDirectory()) {
121
- throw new FsSafeError("invalid-path", "directory moves require overwrite: true");
125
+ else {
126
+ admitMoveSourceStat(fsSync.lstatSync(admittedSourcePath));
122
127
  }
123
128
  assertFinalSymlinkRejected(admittedTargetPath, params.mutationSymlinks !== undefined);
124
129
  params.assertBeforeMutation?.();
@@ -143,6 +148,10 @@ export async function movePathNoReplaceNative(root, params, paths) {
143
148
  for (const admission of parentAdmissions)
144
149
  assertSyncDirectoryGuard(admission.guard);
145
150
  }
151
+ // The callback can change a leaf without replacing its admitted parent.
152
+ if (sourceStat !== undefined) {
153
+ inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(admittedSourcePath, { bigint: true })), sourceStat);
154
+ }
146
155
  try {
147
156
  binding.renameNoReplace(sourceParent.fd, path.basename(paths.sourcePath), targetParent.fd, path.basename(paths.targetPath));
148
157
  }
@@ -50,12 +50,18 @@ export type RootWriteJsonOptions = RootWriteOptions & {
50
50
  space?: Parameters<typeof JSON.stringify>[2];
51
51
  trailingNewline?: boolean;
52
52
  };
53
- export type RootCreateOptions = Omit<RootWriteOptions, "overwrite">;
54
- export type RootCreateStreamOptions = Omit<RootCreateOptions, "encoding" | "renameIdentity"> & {
53
+ export type RootCreateOptions = Omit<RootWriteOptions, "overwrite" | "durable"> & {
54
+ /** Publish complete content without replacing an existing entry, including without native support. */
55
+ atomic?: boolean;
56
+ private?: boolean;
57
+ /** "file" requires file synchronization; directory synchronization remains best effort. */
58
+ durable?: boolean | "file";
59
+ };
60
+ export type RootCreateStreamOptions = Omit<RootCreateOptions, "atomic" | "encoding" | "renameIdentity"> & {
55
61
  maxBytes?: number;
56
62
  signal?: AbortSignal;
57
63
  };
58
- export type RootCreateJsonOptions = Omit<RootWriteJsonOptions, "overwrite">;
64
+ export type RootCreateJsonOptions = Omit<RootWriteJsonOptions, "overwrite" | "durable"> & Pick<RootCreateOptions, "atomic" | "durable" | "private">;
59
65
  export type RootAppendOptions = RootWriteOptions & {
60
66
  prependNewlineIfNeeded?: boolean;
61
67
  };
@@ -70,7 +76,9 @@ export type RootRemoveOptions = Pick<RootDefaults, "assertBeforeMutation" | "den
70
76
  maxDepth?: number;
71
77
  signal?: AbortSignal;
72
78
  };
73
- export type RootMkdirOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks">;
79
+ export type RootMkdirOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
80
+ private?: boolean;
81
+ };
74
82
  export type RootReadParams = Omit<RootReadOptions, "nonBlockingRead">;
75
83
  export declare function readDefaults(defaults: RootDefaults): RootReadParams;
76
84
  export declare function mergeReadOptions(defaults: RootDefaults, options: RootReadOptions): RootReadParams;
@@ -1 +1 @@
1
- {"version":3,"file":"root-options.d.ts","sourceRoot":"","sources":["../src/root-options.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"root-options.d.ts","sourceRoot":"","sources":["../src/root-options.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"root-path-stat.d.ts","sourceRoot":"","sources":["../src/root-path-stat.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAIjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAwB3C,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,0BAA0B,GACnC,OAAO,CAAC,QAAQ,CAAC,CA8EnB"}
1
+ {"version":3,"file":"root-path-stat.d.ts","sourceRoot":"","sources":["../src/root-path-stat.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAIjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AA2D3C,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,0BAA0B,GACnC,OAAO,CAAC,QAAQ,CAAC,CAUnB"}
@@ -1,14 +1,37 @@
1
1
  import fsSync from "node:fs";
2
2
  import path from "node:path";
3
3
  import { FsSafeError } from "./errors.js";
4
- import { recordFileObservationFailure } from "./file-observation.js";
4
+ import { fileObservation, recordFileObservationFailure } from "./file-observation.js";
5
5
  import { hasNodeErrorCode, isNotFoundPathError } from "./path.js";
6
6
  import { assertRootDirectoryObservationGuard, assertRootPathObservationReceiptCurrent, createRootDirectoryObservationGuard, pathStatFromStats, } from "./root-directory-list.js";
7
7
  import { fileNotFoundError } from "./root-errors.js";
8
8
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
9
9
  import { assertStatObservationSync } from "./stat-observation.js";
10
10
  import { getFsSafeTestHooks } from "./test-hooks.js";
11
- async function missingObservedFileError(cause, pathname, before, assertParents) {
11
+ function singleLinkFileIdentity(stat) {
12
+ if (!stat?.isFile() || stat.isSymbolicLink() || (stat.nlink !== 1 && stat.nlink !== 1n))
13
+ return;
14
+ const exact = (value) => typeof value === "bigint" ? value : Number.isSafeInteger(value) ? BigInt(value) : undefined;
15
+ const dev = exact(stat.dev), ino = exact(stat.ino);
16
+ if (dev === undefined || ino === undefined || dev < 0n || ino < 0n ||
17
+ (process.platform === "win32" && (dev === 0n || ino === 0n)))
18
+ return;
19
+ return { dev, ino };
20
+ }
21
+ async function captureChangedObservedFile(error, pathname, before, observed, assertParents, proof) {
22
+ try {
23
+ const previous = singleLinkFileIdentity(before), current = singleLinkFileIdentity(observed);
24
+ if (!previous || !current || (previous.dev === current.dev && previous.ino === current.ino))
25
+ return;
26
+ await assertParents();
27
+ // A changed metadata probe grants only a fresh acquisition attempt, never ownership.
28
+ proof.failure = { error, kind: `stat-leaf-changed:${pathname}` };
29
+ }
30
+ catch {
31
+ // Ambiguous file or ancestry observations retain the original public error.
32
+ }
33
+ }
34
+ async function missingObservedFileError(cause, pathname, before, assertParents, proof) {
12
35
  const failure = new FsSafeError("path-mismatch", "file changed during operation", {
13
36
  cause: cause instanceof Error ? cause : undefined,
14
37
  });
@@ -17,7 +40,7 @@ async function missingObservedFileError(cause, pathname, before, assertParents)
17
40
  try {
18
41
  await assertParents();
19
42
  // Metadata loss permits discarding a probe, never reading or owning a file.
20
- recordFileObservationFailure(failure, `stat-leaf-missing:${pathname}`);
43
+ proof.failure = { error: failure, kind: `stat-leaf-missing:${pathname}` };
21
44
  }
22
45
  catch {
23
46
  // Keep the public mismatch and withhold provenance when ancestry changed.
@@ -26,6 +49,19 @@ async function missingObservedFileError(cause, pathname, before, assertParents)
26
49
  return failure;
27
50
  }
28
51
  export async function statResolvedPathInRoot(root, resolvedPath, receipt) {
52
+ const proof = {};
53
+ try {
54
+ // Nested observations in a hook cannot grant this invocation discard authority.
55
+ return await fileObservation().run(() => statObservedPathInRoot(root, resolvedPath, proof, receipt));
56
+ }
57
+ catch (error) {
58
+ const failure = proof.failure;
59
+ if (failure && failure.error === error)
60
+ recordFileObservationFailure(error, failure.kind);
61
+ throw error;
62
+ }
63
+ }
64
+ async function statObservedPathInRoot(root, resolvedPath, proof, receipt) {
29
65
  let admittedTarget = false;
30
66
  try {
31
67
  if (receipt) {
@@ -37,13 +73,21 @@ export async function statResolvedPathInRoot(root, resolvedPath, receipt) {
37
73
  if (beforeObservation)
38
74
  await beforeObservation(resolvedPath);
39
75
  let observed;
76
+ let sample, samples = 0;
77
+ const comparison = fileObservation();
40
78
  try {
41
- observed = assertStatObservationSync(bigint => bigint ? fsSync.lstatSync(resolvedPath, { bigint: true }) : fsSync.lstatSync(resolvedPath), receipt.target.identity);
79
+ observed = comparison.run(() => assertStatObservationSync(bigint => {
80
+ sample = bigint ? fsSync.lstatSync(resolvedPath, { bigint: true }) : fsSync.lstatSync(resolvedPath);
81
+ samples += 1;
82
+ return sample;
83
+ }, receipt.target.identity));
42
84
  }
43
85
  catch (error) {
44
86
  if (isNotFoundPathError(error)) {
45
- throw await missingObservedFileError(error, resolvedPath, "stat" in receipt.target ? receipt.target.stat : undefined, () => assertRootPathObservationReceiptCurrent(root, receipt));
87
+ throw await missingObservedFileError(error, resolvedPath, "stat" in receipt.target ? receipt.target.stat : undefined, () => assertRootPathObservationReceiptCurrent(root, receipt), proof);
46
88
  }
89
+ if (comparison.has(error, "identity"))
90
+ await captureChangedObservedFile(error, resolvedPath, "stat" in receipt.target ? receipt.target.stat : undefined, samples === 1 ? sample : undefined, () => assertRootPathObservationReceiptCurrent(root, receipt), proof);
47
91
  throw error;
48
92
  }
49
93
  if (observed.isSymbolicLink()) {
@@ -77,13 +121,21 @@ export async function statResolvedPathInRoot(root, resolvedPath, receipt) {
77
121
  if (beforeObservation)
78
122
  await beforeObservation(resolvedPath);
79
123
  let observed;
124
+ let sample, samples = 0;
125
+ const comparison = fileObservation();
80
126
  try {
81
- observed = inspectFileIdentitySync(() => fsSync.lstatSync(resolvedPath, { bigint: true }), expected);
127
+ observed = comparison.run(() => inspectFileIdentitySync(() => {
128
+ sample = fsSync.lstatSync(resolvedPath, { bigint: true });
129
+ samples += 1;
130
+ return sample;
131
+ }, expected));
82
132
  }
83
133
  catch (error) {
84
134
  if (isNotFoundPathError(error)) {
85
- throw await missingObservedFileError(error, resolvedPath, expected, () => assertRootDirectoryObservationGuard(root, parentGuard));
135
+ throw await missingObservedFileError(error, resolvedPath, expected, () => assertRootDirectoryObservationGuard(root, parentGuard), proof);
86
136
  }
137
+ if (comparison.has(error, "identity"))
138
+ await captureChangedObservedFile(error, resolvedPath, expected, samples === 1 ? sample : undefined, () => assertRootDirectoryObservationGuard(root, parentGuard), proof);
87
139
  throw error;
88
140
  }
89
141
  if (observed.isSymbolicLink()) {
@@ -1 +1 @@
1
- {"version":3,"file":"root-read-admission.d.ts","sourceRoot":"","sources":["../src/root-read-admission.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,OAAO,EAAiC,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEpF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAmB9D,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,aAAa,GAAG,SAAS,GAClC,WAAW,CAQb;AA6CD,KAAK,mBAAmB,GAAG;IACzB,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACzB,CAAC;AAEF,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,mBAAmB,EAAE,MAAM,EAAE;IAC/E,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,CAAC;IACV,QAAQ,EAAE,WAAW,CAAC;IACtB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClF,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;CACzE,GAAG,OAAO,CAAC,CAAC,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA8BpC"}
1
+ {"version":3,"file":"root-read-admission.d.ts","sourceRoot":"","sources":["../src/root-read-admission.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,OAAO,EAAiC,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEpF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAmB9D,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,aAAa,GAAG,SAAS,GAClC,WAAW,CAQb;AAgDD,KAAK,mBAAmB,GAAG;IACzB,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACzB,CAAC;AAEF,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,mBAAmB,EAAE,MAAM,EAAE;IAC/E,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,CAAC;IACV,QAAQ,EAAE,WAAW,CAAC;IACtB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClF,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;CACzE,GAAG,OAAO,CAAC,CAAC,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA+BpC"}
@@ -35,7 +35,9 @@ function assertRootReadAdmissionCurrent(params) {
35
35
  assertRootIdentityCurrentSync(params.root);
36
36
  const admittedPath = (() => {
37
37
  try {
38
- inspectFileIdentitySync(() => inspectOpenedPathIdentitySync(params.filePath, params.symlinks), params.identity);
38
+ const current = inspectFileIdentitySync(() => inspectOpenedPathIdentitySync(params.filePath, params.symlinks), params.identity);
39
+ if (params.hardlinks !== "allow" && current.nlink > 1n)
40
+ throw hardlinkedPathNotAllowedError();
39
41
  const canonicalPath = resolveFinalOpenedRealPath(params.filePath);
40
42
  assertNoWindowsPathAlias(canonicalPath, "filesystem", "resolved file path uses a Windows filesystem namespace alias");
41
43
  const admittedRealPath = admitPathInsideRoot({
@@ -48,7 +50,9 @@ function assertRootReadAdmissionCurrent(params) {
48
50
  }
49
51
  // This is a fresh no-follow observation even when canonicalPath and
50
52
  // filePath have the same spelling; realpath may have raced a replacement.
51
- inspectFileIdentitySync(() => inspectOpenedPathIdentitySync(canonicalPath, undefined), params.identity);
53
+ const canonical = inspectFileIdentitySync(() => inspectOpenedPathIdentitySync(canonicalPath, undefined), params.identity);
54
+ if (params.hardlinks !== "allow" && canonical.nlink > 1n)
55
+ throw hardlinkedPathNotAllowedError();
52
56
  return admittedRealPath.path;
53
57
  }
54
58
  catch (error) {
@@ -75,6 +79,7 @@ export async function admitRootReadHandle(params) {
75
79
  filePath: params.filePath,
76
80
  handle: params.opened.handle,
77
81
  identity: params.identity,
82
+ hardlinks: params.hardlinks,
78
83
  symlinks: params.symlinks,
79
84
  afterPathIdentityCheck: params.afterPathIdentityCheck,
80
85
  });
@@ -1 +1 @@
1
- {"version":3,"file":"root-remove.d.ts","sourceRoot":"","sources":["../src/root-remove.ts"],"names":[],"mappings":"AAeA,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAM3D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AASpE,eAAO,MAAM,uBAAuB,EAAE,OAAO,MAA4C,CAAC;AAK1F,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAgBtE;AAyOD,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,iBAAiB,EAC1B,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,IAAI,CAAC,CA6Lf"}
1
+ {"version":3,"file":"root-remove.d.ts","sourceRoot":"","sources":["../src/root-remove.ts"],"names":[],"mappings":"AAeA,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAM3D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AASpE,eAAO,MAAM,uBAAuB,EAAE,OAAO,MAA4C,CAAC;AAK1F,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAgBtE;AAiPD,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,iBAAiB,EAC1B,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAkMf"}
@@ -220,13 +220,21 @@ async function removeOne(root, targetPath, options, receipts) {
220
220
  throw normalizeRemoveGuardError(error);
221
221
  }
222
222
  try {
223
- const isDirectory = fsSync.lstatSync(targetPath).isDirectory();
223
+ const identity = options.assertBeforeMutation
224
+ ? inspectFileIdentitySync(() => fsSync.lstatSync(targetPath, { bigint: true })) : undefined;
225
+ const isDirectory = (identity ?? fsSync.lstatSync(targetPath)).isDirectory();
224
226
  if (!isDirectory && options[nonrecursiveRemovalKind] === "directory") {
225
227
  throw new FsSafeError("path-mismatch", "store entry is no longer a directory");
226
228
  }
227
229
  assertFinalSymlinkRejected(targetPath, options.mutationSymlinks !== undefined);
228
230
  assertNotAborted(options.signal);
229
231
  options.assertBeforeMutation?.();
232
+ if (identity) {
233
+ assertNotAborted(options.signal);
234
+ admission.assertCurrent();
235
+ inspectFileIdentitySync(() => fsSync.lstatSync(targetPath, { bigint: true }), identity);
236
+ assertFinalSymlinkRejected(targetPath, options.mutationSymlinks !== undefined);
237
+ }
230
238
  await (isDirectory ? fs.rmdir(targetPath) : fs.unlink(targetPath));
231
239
  }
232
240
  catch (error) {
@@ -436,6 +444,12 @@ export async function removePathInRootFallback(root, targetPath, options, receip
436
444
  assertFinalSymlinkRejected(target, options.mutationSymlinks !== undefined, details(target, "remove"));
437
445
  assertNotAborted(options.signal);
438
446
  options.assertBeforeMutation?.();
447
+ if (options.assertBeforeMutation) {
448
+ assertNotAborted(options.signal);
449
+ if (!inspect(target, initial))
450
+ return;
451
+ assertFinalSymlinkRejected(target, options.mutationSymlinks !== undefined, details(target, "remove"));
452
+ }
439
453
  try {
440
454
  await (initial.isDirectory() ? fs.rmdir(target) : fs.unlink(target));
441
455
  }
@@ -4,7 +4,7 @@ import { readFileHandleBounded } from "./bounded-read.js";
4
4
  import { syncDirectoryBestEffort } from "./directory-durability.js";
5
5
  import { FsSafeError } from "./errors.js";
6
6
  import { sameFileIdentityForCleanup, sha256Hex } from "./file-identity.js";
7
- import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
7
+ import { cleanupPinnedFilePath } from "./file-cleanup.js";
8
8
  import { verifyAtomicWriteResult } from "./root-write-verification.js";
9
9
  // Compatibility callers hold the lock throughout this operation. The caller
10
10
  // owns the original write handle; this helper owns a content-verified reopen.
@@ -1 +1 @@
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,EAIL,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAQjC,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,CA2CR;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;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AA+BF,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,CAgCD;AAwDD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBzF"}
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,EAIL,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAQjC,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,CA2CR;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;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AA+BF,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,CAgCD;AAyDD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBzF"}
@@ -291,6 +291,7 @@ async function materializeSecretFileAtomic(params, createOnly) {
291
291
  basename: fileName,
292
292
  mkdir: false,
293
293
  mode,
294
+ verifyPosixMode: true,
294
295
  sync: params.durable !== false,
295
296
  overwrite: !createOnly,
296
297
  input: { kind: "buffer", data: typeof params.content === "string" ? params.content : Buffer.from(params.content) },
@@ -6,5 +6,11 @@ export declare function inspectSecureWindowsDescriptor(params: {
6
6
  identity: ExactIdentity;
7
7
  stat: Stats;
8
8
  }): PermissionCheck;
9
+ /** Both mechanisms inspect the same borrowed handle; the caller owns its lifetime. */
10
+ export declare function inspectSecureWindowsFile(params: {
11
+ fd: number;
12
+ identity: ExactIdentity;
13
+ stat: Stats;
14
+ }): Promise<PermissionCheck>;
9
15
  export {};
10
16
  //# sourceMappingURL=secure-file-windows.d.ts.map