@openclaw/fs-safe 0.5.6 → 0.7.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 (289) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +23 -10
  3. package/dist/advanced.d.ts +2 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -0
  6. package/dist/archive-crc32.d.ts +2 -0
  7. package/dist/archive-crc32.d.ts.map +1 -0
  8. package/dist/archive-crc32.js +14 -0
  9. package/dist/archive-deadline.d.ts +3 -0
  10. package/dist/archive-deadline.d.ts.map +1 -1
  11. package/dist/archive-deadline.js +44 -8
  12. package/dist/archive-entry.d.ts.map +1 -1
  13. package/dist/archive-entry.js +1 -0
  14. package/dist/archive-errors.d.ts +1 -0
  15. package/dist/archive-errors.d.ts.map +1 -1
  16. package/dist/archive-errors.js +3 -0
  17. package/dist/archive-input.d.ts.map +1 -1
  18. package/dist/archive-input.js +26 -16
  19. package/dist/archive-kind.js +2 -2
  20. package/dist/archive-limits.d.ts +11 -3
  21. package/dist/archive-limits.d.ts.map +1 -1
  22. package/dist/archive-limits.js +24 -0
  23. package/dist/archive-native.d.ts +3 -2
  24. package/dist/archive-native.d.ts.map +1 -1
  25. package/dist/archive-native.js +31 -9
  26. package/dist/archive-policy.d.ts +2 -0
  27. package/dist/archive-policy.d.ts.map +1 -1
  28. package/dist/archive-policy.js +9 -1
  29. package/dist/archive-read.d.ts.map +1 -1
  30. package/dist/archive-read.js +88 -41
  31. package/dist/archive-staging.d.ts +3 -0
  32. package/dist/archive-staging.d.ts.map +1 -1
  33. package/dist/archive-staging.js +91 -43
  34. package/dist/archive-tar-admission.d.ts +7 -0
  35. package/dist/archive-tar-admission.d.ts.map +1 -0
  36. package/dist/archive-tar-admission.js +43 -0
  37. package/dist/archive-tar-gnu.d.ts +2 -0
  38. package/dist/archive-tar-gnu.d.ts.map +1 -0
  39. package/dist/archive-tar-gnu.js +20 -0
  40. package/dist/archive-tar-header.d.ts +8 -0
  41. package/dist/archive-tar-header.d.ts.map +1 -0
  42. package/dist/archive-tar-header.js +47 -0
  43. package/dist/archive-tar-meta.d.ts +29 -1
  44. package/dist/archive-tar-meta.d.ts.map +1 -1
  45. package/dist/archive-tar-meta.js +135 -19
  46. package/dist/archive-tar-pax.d.ts +8 -0
  47. package/dist/archive-tar-pax.d.ts.map +1 -0
  48. package/dist/archive-tar-pax.js +100 -0
  49. package/dist/archive-tar-runtime.d.ts +4 -0
  50. package/dist/archive-tar-runtime.d.ts.map +1 -1
  51. package/dist/archive-tar-runtime.js +3 -0
  52. package/dist/archive-tar.d.ts.map +1 -1
  53. package/dist/archive-tar.js +8 -2
  54. package/dist/archive-zip-admission.d.ts +7 -0
  55. package/dist/archive-zip-admission.d.ts.map +1 -0
  56. package/dist/archive-zip-admission.js +60 -0
  57. package/dist/archive-zip-count.d.ts +2 -0
  58. package/dist/archive-zip-count.d.ts.map +1 -0
  59. package/dist/archive-zip-count.js +140 -0
  60. package/dist/archive-zip-directory.d.ts +8 -0
  61. package/dist/archive-zip-directory.d.ts.map +1 -0
  62. package/dist/archive-zip-directory.js +224 -0
  63. package/dist/archive-zip-integrity.d.ts.map +1 -1
  64. package/dist/archive-zip-integrity.js +1 -14
  65. package/dist/archive-zip-names.d.ts +12 -0
  66. package/dist/archive-zip-names.d.ts.map +1 -0
  67. package/dist/archive-zip-names.js +99 -0
  68. package/dist/archive-zip-preflight.d.ts +1 -1
  69. package/dist/archive-zip-preflight.d.ts.map +1 -1
  70. package/dist/archive-zip-preflight.js +8 -145
  71. package/dist/archive.d.ts.map +1 -1
  72. package/dist/archive.js +63 -42
  73. package/dist/atomic.d.ts +1 -1
  74. package/dist/atomic.d.ts.map +1 -1
  75. package/dist/bounded-read-stream.d.ts.map +1 -1
  76. package/dist/bounded-read-stream.js +2 -4
  77. package/dist/bounded-read.d.ts.map +1 -1
  78. package/dist/bounded-read.js +3 -10
  79. package/dist/byte-budget.d.ts +5 -0
  80. package/dist/byte-budget.d.ts.map +1 -0
  81. package/dist/byte-budget.js +9 -0
  82. package/dist/file-hash.d.ts.map +1 -1
  83. package/dist/file-hash.js +25 -19
  84. package/dist/file-lock-sync.d.ts.map +1 -1
  85. package/dist/file-lock-sync.js +26 -9
  86. package/dist/file-store-boundary.d.ts.map +1 -1
  87. package/dist/file-store-boundary.js +9 -5
  88. package/dist/file-store-limit.d.ts +2 -0
  89. package/dist/file-store-limit.d.ts.map +1 -0
  90. package/dist/file-store-limit.js +8 -0
  91. package/dist/file-store-sync-write.d.ts.map +1 -1
  92. package/dist/file-store-sync-write.js +38 -8
  93. package/dist/file-store.d.ts.map +1 -1
  94. package/dist/file-store.js +26 -26
  95. package/dist/json-durable-queue-directory.d.ts +2 -0
  96. package/dist/json-durable-queue-directory.d.ts.map +1 -0
  97. package/dist/json-durable-queue-directory.js +20 -0
  98. package/dist/json-durable-queue-ownership.d.ts +14 -0
  99. package/dist/json-durable-queue-ownership.d.ts.map +1 -0
  100. package/dist/json-durable-queue-ownership.js +168 -0
  101. package/dist/json-durable-queue-retirement.d.ts +9 -0
  102. package/dist/json-durable-queue-retirement.d.ts.map +1 -0
  103. package/dist/json-durable-queue-retirement.js +126 -0
  104. package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
  105. package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
  106. package/dist/json-durable-queue-transfer-lock.js +19 -0
  107. package/dist/json-durable-queue.d.ts +1 -0
  108. package/dist/json-durable-queue.d.ts.map +1 -1
  109. package/dist/json-durable-queue.js +90 -57
  110. package/dist/json.d.ts.map +1 -1
  111. package/dist/json.js +27 -8
  112. package/dist/local-roots.d.ts.map +1 -1
  113. package/dist/local-roots.js +4 -2
  114. package/dist/native-binding.d.ts +15 -3
  115. package/dist/native-binding.d.ts.map +1 -1
  116. package/dist/native-operations.d.ts +4 -1
  117. package/dist/native-operations.d.ts.map +1 -1
  118. package/dist/native-operations.js +22 -6
  119. package/dist/native-pinned-write-windows.d.ts +8 -0
  120. package/dist/native-pinned-write-windows.d.ts.map +1 -0
  121. package/dist/native-pinned-write-windows.js +92 -0
  122. package/dist/native-pinned-write.d.ts.map +1 -1
  123. package/dist/native-pinned-write.js +136 -127
  124. package/dist/native-staged-file.d.ts +24 -0
  125. package/dist/native-staged-file.d.ts.map +1 -0
  126. package/dist/native-staged-file.js +337 -0
  127. package/dist/native.d.ts.map +1 -1
  128. package/dist/native.js +4 -4
  129. package/dist/opened-realpath.d.ts +2 -0
  130. package/dist/opened-realpath.d.ts.map +1 -1
  131. package/dist/opened-realpath.js +12 -7
  132. package/dist/output-sibling.d.ts.map +1 -1
  133. package/dist/output-sibling.js +11 -110
  134. package/dist/output.d.ts.map +1 -1
  135. package/dist/output.js +4 -2
  136. package/dist/owner-dacl.d.ts.map +1 -1
  137. package/dist/owner-dacl.js +2 -1
  138. package/dist/permission-exec.d.ts +19 -0
  139. package/dist/permission-exec.d.ts.map +1 -1
  140. package/dist/permission-exec.js +57 -11
  141. package/dist/permissions-public.d.ts +1 -1
  142. package/dist/permissions-public.d.ts.map +1 -1
  143. package/dist/permissions-windows.d.ts +3 -0
  144. package/dist/permissions-windows.d.ts.map +1 -1
  145. package/dist/permissions-windows.js +16 -5
  146. package/dist/permissions.d.ts +5 -0
  147. package/dist/permissions.d.ts.map +1 -1
  148. package/dist/pinned-open.d.ts.map +1 -1
  149. package/dist/pinned-open.js +27 -48
  150. package/dist/pinned-write.d.ts +6 -0
  151. package/dist/pinned-write.d.ts.map +1 -1
  152. package/dist/pinned-write.js +26 -37
  153. package/dist/private-directory.d.ts.map +1 -1
  154. package/dist/private-directory.js +3 -2
  155. package/dist/private-temp-workspace.d.ts +3 -1
  156. package/dist/private-temp-workspace.d.ts.map +1 -1
  157. package/dist/private-temp-workspace.js +81 -56
  158. package/dist/publish-file.d.ts.map +1 -1
  159. package/dist/publish-file.js +2 -4
  160. package/dist/read-opened-file.d.ts.map +1 -1
  161. package/dist/read-opened-file.js +6 -4
  162. package/dist/regular-file.d.ts.map +1 -1
  163. package/dist/regular-file.js +136 -82
  164. package/dist/replace-file-copy-fallback.d.ts +3 -1
  165. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  166. package/dist/replace-file-copy-fallback.js +21 -36
  167. package/dist/replace-file-copy-source.d.ts +21 -0
  168. package/dist/replace-file-copy-source.d.ts.map +1 -0
  169. package/dist/replace-file-copy-source.js +112 -0
  170. package/dist/replace-file-descriptor.d.ts +13 -3
  171. package/dist/replace-file-descriptor.d.ts.map +1 -1
  172. package/dist/replace-file-descriptor.js +32 -7
  173. package/dist/replace-file-rename-policy.d.ts +7 -0
  174. package/dist/replace-file-rename-policy.d.ts.map +1 -0
  175. package/dist/replace-file-rename-policy.js +30 -0
  176. package/dist/replace-file-temp-owner.d.ts +46 -0
  177. package/dist/replace-file-temp-owner.d.ts.map +1 -0
  178. package/dist/replace-file-temp-owner.js +346 -0
  179. package/dist/replace-file.d.ts +6 -1
  180. package/dist/replace-file.d.ts.map +1 -1
  181. package/dist/replace-file.js +72 -58
  182. package/dist/root-impl.d.ts.map +1 -1
  183. package/dist/root-impl.js +118 -98
  184. package/dist/root-paths.d.ts +11 -14
  185. package/dist/root-paths.d.ts.map +1 -1
  186. package/dist/root-paths.js +36 -27
  187. package/dist/root-write-verification.d.ts +11 -0
  188. package/dist/root-write-verification.d.ts.map +1 -0
  189. package/dist/root-write-verification.js +91 -0
  190. package/dist/secret-file.d.ts +1 -6
  191. package/dist/secret-file.d.ts.map +1 -1
  192. package/dist/secret-file.js +49 -120
  193. package/dist/secret-read-async.d.ts +1 -1
  194. package/dist/secret-read-async.d.ts.map +1 -1
  195. package/dist/secret-read-async.js +51 -72
  196. package/dist/secret-read-policy.d.ts +13 -0
  197. package/dist/secret-read-policy.d.ts.map +1 -0
  198. package/dist/secret-read-policy.js +28 -0
  199. package/dist/secret.d.ts +2 -1
  200. package/dist/secret.d.ts.map +1 -1
  201. package/dist/secret.js +2 -1
  202. package/dist/secure-file.d.ts.map +1 -1
  203. package/dist/secure-file.js +42 -29
  204. package/dist/sibling-staged-file.d.ts +15 -0
  205. package/dist/sibling-staged-file.d.ts.map +1 -0
  206. package/dist/sibling-staged-file.js +148 -0
  207. package/dist/sibling-temp.d.ts +3 -0
  208. package/dist/sibling-temp.d.ts.map +1 -1
  209. package/dist/sibling-temp.js +30 -74
  210. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  211. package/dist/sidecar-lock-acquire.js +48 -27
  212. package/dist/sidecar-lock-handle.d.ts +6 -2
  213. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  214. package/dist/sidecar-lock-handle.js +17 -3
  215. package/dist/sidecar-lock-policy.d.ts +2 -0
  216. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  217. package/dist/sidecar-lock-policy.js +29 -0
  218. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  219. package/dist/sidecar-lock-reclaim.js +29 -7
  220. package/dist/sidecar-lock.d.ts.map +1 -1
  221. package/dist/sidecar-lock.js +28 -16
  222. package/dist/staged-directory.d.ts +16 -0
  223. package/dist/staged-directory.d.ts.map +1 -0
  224. package/dist/staged-directory.js +60 -0
  225. package/dist/staged-file-types.d.ts +56 -0
  226. package/dist/staged-file-types.d.ts.map +1 -0
  227. package/dist/staged-file-types.js +1 -0
  228. package/dist/staged-file.d.ts +10 -0
  229. package/dist/staged-file.d.ts.map +1 -0
  230. package/dist/staged-file.js +15 -0
  231. package/dist/strict-file-identity.d.ts +6 -0
  232. package/dist/strict-file-identity.d.ts.map +1 -0
  233. package/dist/strict-file-identity.js +48 -0
  234. package/dist/suppressed-error.d.ts +6 -0
  235. package/dist/suppressed-error.d.ts.map +1 -0
  236. package/dist/suppressed-error.js +15 -0
  237. package/dist/temp-cleanup.d.ts +2 -0
  238. package/dist/temp-cleanup.d.ts.map +1 -1
  239. package/dist/temp-cleanup.js +25 -10
  240. package/dist/temp-workspace-owner.d.ts +23 -0
  241. package/dist/temp-workspace-owner.d.ts.map +1 -0
  242. package/dist/temp-workspace-owner.js +320 -0
  243. package/dist/temp.d.ts +1 -1
  244. package/dist/temp.d.ts.map +1 -1
  245. package/dist/test-hooks.d.ts +5 -0
  246. package/dist/test-hooks.d.ts.map +1 -1
  247. package/dist/windows-owner.d.ts +3 -0
  248. package/dist/windows-owner.d.ts.map +1 -1
  249. package/dist/windows-owner.js +10 -2
  250. package/docs/advanced.md +19 -2
  251. package/docs/archive.md +250 -35
  252. package/docs/atomic.md +11 -2
  253. package/docs/config.md +7 -0
  254. package/docs/contributing.md +45 -7
  255. package/docs/durability.md +18 -5
  256. package/docs/errors.md +16 -1
  257. package/docs/file-store.md +2 -0
  258. package/docs/index.md +3 -1
  259. package/docs/install.md +20 -8
  260. package/docs/json.md +8 -4
  261. package/docs/migrating-to-0.5.md +7 -7
  262. package/docs/migrating-to-0.6.md +43 -0
  263. package/docs/native-helper.md +30 -8
  264. package/docs/native.md +73 -17
  265. package/docs/output.md +10 -0
  266. package/docs/path-scope.md +28 -2
  267. package/docs/permissions.md +13 -2
  268. package/docs/public-api.md +6 -3
  269. package/docs/quickstart.md +1 -1
  270. package/docs/reading.md +1 -1
  271. package/docs/regular-file.md +9 -2
  272. package/docs/root.md +3 -1
  273. package/docs/secret-file.md +12 -0
  274. package/docs/secure-file.md +21 -3
  275. package/docs/security-model.md +33 -1
  276. package/docs/sidecar-lock.md +15 -1
  277. package/docs/staged-file.md +178 -0
  278. package/docs/store.md +13 -0
  279. package/docs/temp.md +128 -14
  280. package/docs/testing.md +1 -1
  281. package/docs/writing.md +19 -0
  282. package/package.json +16 -9
  283. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  284. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  285. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  286. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  287. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  288. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  289. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
package/dist/archive.js CHANGED
@@ -3,9 +3,9 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { Readable } from "node:stream";
5
5
  import { pipeline } from "node:stream/promises";
6
- import { createArchiveOutputPathTracker, normalizeArchiveEntryPath, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
6
+ import { createArchiveOutputPathTracker, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
7
7
  import { createPipelineTimeoutError, waitForDeadline, withExtractionDeadline, } from "./archive-deadline.js";
8
- import { assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, createExtractBudgetTransform, resolveExtractLimits, } from "./archive-limits.js";
8
+ import { assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, createExtractBudgetTransform, resolveExtractLimits, resolveTarMeterLimits, } from "./archive-limits.js";
9
9
  import { resolveArchiveKind } from "./archive-kind.js";
10
10
  import { mergeExtractedTreeIntoDestination, prepareArchiveDestinationDir, prepareArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
11
11
  import { createTarEntryPreflightChecker, readTarEntryInfo, } from "./archive-tar.js";
@@ -17,9 +17,10 @@ import { ArchiveSecurityError } from "./archive-errors.js";
17
17
  import { extractNativeArchive } from "./archive-native.js";
18
18
  import { stageArchiveFileForExtraction } from "./archive-input.js";
19
19
  import { getNativeBinding } from "./native.js";
20
- import { resolveArchiveEntryMode, shouldExtractArchiveEntry, } from "./archive-policy.js";
21
- import { importOptionalTar, normalizeTarParserError } from "./archive-tar-runtime.js";
20
+ import { resolveArchiveEntryMode, resolveArchiveFilteredEntryPolicy, shouldExtractArchiveEntry, } from "./archive-policy.js";
21
+ import { importOptionalTar, NODE_TAR_RATIO_DISABLED, normalizeTarParserError } from "./archive-tar-runtime.js";
22
22
  import { preflightTarMetadata } from "./archive-tar-meta.js";
23
+ import { createTarAdmissionPlan } from "./archive-tar-admission.js";
23
24
  import { writeSiblingTempFile } from "./sibling-temp.js";
24
25
  export { isWindowsDrivePath, normalizeArchiveEntryPath, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
25
26
  export { resolveArchiveKind, resolvePackedRootDir } from "./archive-kind.js";
@@ -45,12 +46,16 @@ async function readZipEntryStream(entry) {
45
46
  }
46
47
  function resolveZipOutputPath(params) {
47
48
  validateArchiveEntryPath(params.entryPath);
48
- const relPath = stripArchivePath(params.entryPath, params.strip);
49
+ const canonicalPath = stripArchivePath(params.entryPath, 0);
50
+ if (!canonicalPath)
51
+ return null;
52
+ const relPath = stripArchivePath(canonicalPath, params.strip);
49
53
  if (!relPath) {
50
54
  return null;
51
55
  }
52
56
  validateArchiveEntryPath(relPath);
53
57
  return {
58
+ canonicalPath,
54
59
  relPath,
55
60
  outPath: resolveArchiveOutputPath({
56
61
  rootDir: params.destinationDir,
@@ -151,10 +156,13 @@ async function extractZip(params) {
151
156
  if (!shouldExtractArchiveEntry({
152
157
  filter: params.entryFilter,
153
158
  onFiltered: params.onFiltered,
154
- entry: { path: entry.name, kind: entryKind, size: entrySize },
159
+ entry: { path: output.canonicalPath, kind: entryKind, size: entrySize },
155
160
  })) {
156
161
  continue;
157
162
  }
163
+ if (isSymlink) {
164
+ throw new ArchiveSecurityError("entry-link", `zip entry is a link: ${entry.name}`);
165
+ }
158
166
  const mode = zipEntryMode(entry, params.entryModes);
159
167
  await prepareZipOutputPath({
160
168
  destinationDir: stagingRealDir,
@@ -163,14 +171,12 @@ async function extractZip(params) {
163
171
  outPath: output.outPath,
164
172
  originalPath: entry.name,
165
173
  isDirectory: entry.dir,
174
+ deadline: params.deadline,
166
175
  });
167
176
  if (entry.dir) {
168
177
  await fs.chmod(output.outPath, mode);
169
178
  continue;
170
179
  }
171
- if (isSymlink) {
172
- throw new ArchiveSecurityError("entry-link", `zip entry is a link: ${entry.name}`);
173
- }
174
180
  await writeZipFileEntry({
175
181
  entry,
176
182
  outPath: output.outPath,
@@ -184,6 +190,7 @@ async function extractZip(params) {
184
190
  sourceDir: stagingRealDir,
185
191
  destinationDir: params.destDir,
186
192
  destinationRealDir,
193
+ deadline: params.deadline,
187
194
  });
188
195
  params.deadline.check();
189
196
  },
@@ -194,11 +201,14 @@ async function extractZip(params) {
194
201
  }
195
202
  }
196
203
  export async function extractArchive(params) {
204
+ const onFiltered = resolveArchiveFilteredEntryPolicy(params.onFiltered);
197
205
  const kind = params.kind ?? resolveArchiveKind(params.archivePath);
198
206
  if (!kind) {
199
207
  throw new Error(`unsupported archive: ${params.archivePath}`);
200
208
  }
201
209
  const label = kind === "zip" ? "extract zip" : "extract tar";
210
+ const limits = resolveExtractLimits(params.limits);
211
+ const tarLimits = resolveTarMeterLimits(limits);
202
212
  const native = getNativeBinding();
203
213
  if (native) {
204
214
  await withExtractionDeadline(params.timeoutMs, label, async (deadline) => extractNativeArchive({
@@ -207,31 +217,34 @@ export async function extractArchive(params) {
207
217
  destDir: params.destDir,
208
218
  kind,
209
219
  stripComponents: params.stripComponents,
210
- limits: params.limits,
220
+ limits,
221
+ tarLimits,
211
222
  deadline,
212
223
  entryModes: params.entryModes,
213
224
  entryFilter: params.entryFilter,
214
- onFiltered: params.onFiltered,
225
+ onFiltered,
215
226
  }));
216
227
  return;
217
228
  }
218
229
  if (kind === "tar-zstd" || kind === "tar-bzip2") {
219
- throw new FsSafeError("helper-unavailable", `${kind} archives require a supported bundled native binding`);
230
+ throw new FsSafeError("helper-unavailable", `${kind} archives require the matching optional native platform package; ` +
231
+ "install @openclaw/fs-safe with optional dependencies enabled on a supported platform and use FS_SAFE_NATIVE_MODE=auto or require");
220
232
  }
221
233
  if (kind === "tar") {
222
234
  await withExtractionDeadline(params.timeoutMs, label, async (deadline) => {
223
235
  const tar = await importOptionalTar();
224
- const limits = resolveExtractLimits(params.limits);
225
236
  const stagedArchive = await stageArchiveFileForExtraction({
226
237
  archivePath: params.archivePath,
227
238
  limits,
228
239
  deadline,
229
240
  });
230
241
  try {
242
+ const manifest = [];
231
243
  await preflightTarMetadata({
232
244
  archivePath: stagedArchive.path,
233
- maxMetaEntryBytes: limits.maxMetaEntryBytes,
245
+ limits: tarLimits,
234
246
  signal: deadline.signal,
247
+ onMember: (entry) => { manifest.push(entry); },
235
248
  });
236
249
  deadline.check();
237
250
  const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
@@ -239,49 +252,54 @@ export async function extractArchive(params) {
239
252
  destinationRealDir,
240
253
  run: async (stagingDir) => {
241
254
  deadline.check();
255
+ const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
242
256
  const checkTarEntrySafety = createTarEntryPreflightChecker({
243
257
  rootDir: destinationRealDir,
244
258
  stripComponents: params.stripComponents,
245
259
  limits,
246
260
  entryFilter: params.entryFilter,
247
- onFiltered: params.onFiltered,
261
+ onFiltered,
248
262
  });
263
+ const admission = createTarAdmissionPlan(manifest, (entry) => {
264
+ deadline.check();
265
+ return checkTarEntrySafety(entry);
266
+ }, strip);
249
267
  const acceptedEntries = [];
250
- // A canonical cwd is not enough here: tar can still follow
251
- // pre-existing child symlinks in the live destination tree.
252
- // Extract into a private staging dir first, then merge through
253
- // the same safe-open boundary checks used by direct file writes.
268
+ // Extract privately, then merge through the safe-open boundary.
254
269
  const extractor = tar.x({
255
270
  cwd: stagingDir,
256
- strip: Math.max(0, Math.floor(params.stripComponents ?? 0)),
271
+ // fs-safe owns stripping: node-tar counts the `.` and empty path
272
+ // components that stripArchivePath() drops, so the two disagree.
273
+ strip: 0,
257
274
  gzip: params.tarGzip,
258
275
  signal: deadline.signal,
259
276
  preservePaths: false,
260
277
  noChmod: true,
261
278
  preserveOwner: false,
279
+ noMtime: true,
262
280
  strict: true,
263
- maxMetaEntrySize: limits.maxMetaEntryBytes,
281
+ maxMetaEntrySize: tarLimits.maxMetaEntryBytes,
282
+ maxDecompressionRatio: NODE_TAR_RATIO_DISABLED,
264
283
  filter(_entryPath, entry) {
265
284
  try {
266
285
  const info = readTarEntryInfo(entry);
267
- const accepted = checkTarEntrySafety(info);
268
- if (accepted) {
269
- entry.path = normalizeArchiveEntryPath(info.path);
270
- const relPath = stripArchivePath(info.path, Math.max(0, Math.floor(params.stripComponents ?? 0)));
271
- if (relPath) {
272
- acceptedEntries.push({
273
- path: relPath,
274
- mode: resolveArchiveEntryMode({
275
- kind: info.type === "Directory" || info.type === "GNUDumpDir"
276
- ? "directory"
277
- : "file",
278
- archivedMode: info.mode,
279
- policy: params.entryModes,
280
- }),
281
- });
282
- }
286
+ const relPath = admission.consume(info);
287
+ if (!relPath) {
288
+ return false;
283
289
  }
284
- return accepted;
290
+ // Writes and the mode pass use the admitted output identity.
291
+ entry.path = relPath;
292
+ acceptedEntries.push({
293
+ path: relPath,
294
+ mode: resolveArchiveEntryMode({
295
+ kind: info.type === "Directory" || info.type === "GNUDumpDir"
296
+ ? "directory"
297
+ : "file",
298
+ archivedMode: info.mode,
299
+ policy: params.entryModes,
300
+ }),
301
+ });
302
+ return true;
285
303
  }
286
304
  catch (error) {
287
305
  // Abort through the parser so pipeline tears down both the
@@ -297,8 +315,7 @@ export async function extractArchive(params) {
297
315
  }
298
316
  catch (err) {
299
317
  const error = err instanceof Error ? err : new Error(String(err));
300
- // Node's EventEmitter calls listeners with `this` bound to the
301
- // emitter (tar.Unpack), which exposes Parser.abort().
318
+ // EventEmitter binds `this` to tar.Unpack, exposing abort().
302
319
  const emitter = this;
303
320
  emitter.abort?.(error);
304
321
  }
@@ -312,19 +329,23 @@ export async function extractArchive(params) {
312
329
  catch (error) {
313
330
  throw normalizeTarParserError(createPipelineTimeoutError(error, deadline));
314
331
  }
332
+ admission.finish();
315
333
  for (const accepted of acceptedEntries) {
334
+ deadline.check();
316
335
  const outputPath = resolveArchiveOutputPath({
317
336
  rootDir: stagingDir,
318
337
  relPath: accepted.path,
319
338
  originalPath: accepted.path,
320
339
  });
321
340
  await fs.chmod(outputPath, accepted.mode);
341
+ deadline.check();
322
342
  }
323
343
  deadline.check();
324
344
  await mergeExtractedTreeIntoDestination({
325
345
  sourceDir: stagingDir,
326
346
  destinationDir: params.destDir,
327
347
  destinationRealDir,
348
+ deadline,
328
349
  });
329
350
  deadline.check();
330
351
  },
@@ -340,10 +361,10 @@ export async function extractArchive(params) {
340
361
  archivePath: params.archivePath,
341
362
  destDir: params.destDir,
342
363
  stripComponents: params.stripComponents,
343
- limits: params.limits,
364
+ limits,
344
365
  deadline,
345
366
  entryModes: params.entryModes,
346
367
  entryFilter: params.entryFilter,
347
- onFiltered: params.onFiltered,
368
+ onFiltered,
348
369
  }));
349
370
  }
package/dist/atomic.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { replaceFileAtomic, replaceFileAtomicSync, type ReplaceFileAtomicFileSystem, type ReplaceFileAtomicOptions, type ReplaceFileAtomicRestoreCleanup, type ReplaceFileAtomicRestoreFailureDetails, type ReplaceFileAtomicResult, type ReplaceFileCopyFallbackRestorePolicy, type ReplaceFileDestinationHardlinkPolicy, type ReplaceFileAtomicSyncFileSystem, type ReplaceFileAtomicSyncOptions, } from "./replace-file.js";
1
+ export { replaceFileAtomic, replaceFileAtomicSync, type RenameIdentityPolicy, type ReplaceFileAtomicFileSystem, type ReplaceFileAtomicOptions, type ReplaceFileAtomicRestoreCleanup, type ReplaceFileAtomicRestoreFailureDetails, type ReplaceFileAtomicResult, type ReplaceFileCopyFallbackRestorePolicy, type ReplaceFileDestinationHardlinkPolicy, type ReplaceFileAtomicSyncFileSystem, type ReplaceFileAtomicSyncOptions, } from "./replace-file.js";
2
2
  export { writeTextAtomic, type WriteTextAtomicOptions } from "./text-atomic.js";
3
3
  export { replaceDirectoryAtomic, type ReplaceDirectoryAtomicOptions } from "./replace-directory.js";
4
4
  export { movePathWithCopyFallback, type MovePathWithCopyFallbackOptions } from "./move-path.js";
@@ -1 +1 @@
1
- {"version":3,"file":"atomic.d.ts","sourceRoot":"","sources":["../src/atomic.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACpC,KAAK,sCAAsC,EAC3C,KAAK,uBAAuB,EAC5B,KAAK,oCAAoC,EACzC,KAAK,oCAAoC,EACzC,KAAK,+BAA+B,EACpC,KAAK,4BAA4B,GAClC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,KAAK,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AACpG,OAAO,EAAE,wBAAwB,EAAE,KAAK,+BAA+B,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"atomic.d.ts","sourceRoot":"","sources":["../src/atomic.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACpC,KAAK,sCAAsC,EAC3C,KAAK,uBAAuB,EAC5B,KAAK,oCAAoC,EACzC,KAAK,oCAAoC,EACzC,KAAK,+BAA+B,EACpC,KAAK,4BAA4B,GAClC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,KAAK,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AACpG,OAAO,EAAE,wBAAwB,EAAE,KAAK,+BAA+B,EAAE,MAAM,gBAAgB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"bounded-read-stream.d.ts","sourceRoot":"","sources":["../src/bounded-read-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,SAAS,EAAE,MAAM,aAAa,CAAC;AAGlD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAwBnE;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE;IAAE,MAAM,EAAE;QAAE,gBAAgB,IAAI,MAAM,CAAC,cAAc,CAAA;KAAE,CAAA;CAAE,EACjE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAAC,cAAc,CAWvB"}
1
+ {"version":3,"file":"bounded-read-stream.d.ts","sourceRoot":"","sources":["../src/bounded-read-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,SAAS,EAAE,MAAM,aAAa,CAAC;AAIlD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAmBnE;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE;IAAE,MAAM,EAAE;QAAE,gBAAgB,IAAI,MAAM,CAAC,cAAc,CAAA;KAAE,CAAA;CAAE,EACjE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAAC,cAAc,CAWvB"}
@@ -1,10 +1,8 @@
1
1
  import { pipeline, Transform } from "node:stream";
2
+ import { normalizeMaxBytes } from "./byte-budget.js";
2
3
  import { FsSafeError } from "./errors.js";
3
4
  export function createMaxBytesTransform(maxBytes) {
4
- if (maxBytes !== Number.POSITIVE_INFINITY &&
5
- (!Number.isSafeInteger(maxBytes) || maxBytes < 0)) {
6
- throw new RangeError("maxBytes must be a non-negative safe integer or Infinity");
7
- }
5
+ normalizeMaxBytes(maxBytes);
8
6
  let bytes = 0;
9
7
  return new Transform({
10
8
  transform(chunk, _encoding, callback) {
@@ -1 +1 @@
1
- {"version":3,"file":"bounded-read.d.ts","sourceRoot":"","sources":["../src/bounded-read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,KAAK,kBAAkB,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AA4DnD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAIjB;AAcD,oFAAoF;AACpF,wBAAsB,yBAAyB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI7F;AAED,mFAAmF;AACnF,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAalF"}
1
+ {"version":3,"file":"bounded-read.d.ts","sourceRoot":"","sources":["../src/bounded-read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD,KAAK,kBAAkB,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AAmDnD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAIjB;AAcD,oFAAoF;AACpF,wBAAsB,yBAAyB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI7F;AAED,mFAAmF;AACnF,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAalF"}
@@ -1,14 +1,7 @@
1
1
  import fs from "node:fs";
2
+ import { normalizeMaxBytes } from "./byte-budget.js";
2
3
  import { FsSafeError } from "./errors.js";
3
4
  const READ_CHUNK_BYTES = 64 * 1024;
4
- function assertMaxBytes(maxBytes) {
5
- if (maxBytes === Number.POSITIVE_INFINITY) {
6
- return;
7
- }
8
- if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
9
- throw new RangeError("maxBytes must be a non-negative safe integer or Infinity");
10
- }
11
- }
12
5
  function createScratchBuffer(maxBytes) {
13
6
  const initialReadBytes = Number.isFinite(maxBytes)
14
7
  ? Math.min(READ_CHUNK_BYTES, maxBytes + 1)
@@ -29,7 +22,7 @@ function appendChunk(params) {
29
22
  return total;
30
23
  }
31
24
  async function readBoundedAsync(maxBytes, readChunk) {
32
- assertMaxBytes(maxBytes);
25
+ normalizeMaxBytes(maxBytes);
33
26
  const chunks = [];
34
27
  const scratch = createScratchBuffer(maxBytes);
35
28
  let total = 0;
@@ -71,7 +64,7 @@ export async function readFileDescriptorBounded(fd, maxBytes) {
71
64
  }
72
65
  /** Sync bounded read from a numeric descriptor. The caller owns the descriptor. */
73
66
  export function readFileDescriptorBoundedSync(fd, maxBytes) {
74
- assertMaxBytes(maxBytes);
67
+ normalizeMaxBytes(maxBytes);
75
68
  const chunks = [];
76
69
  const scratch = createScratchBuffer(maxBytes);
77
70
  let total = 0;
@@ -0,0 +1,5 @@
1
+ export declare function normalizeMaxBytes(value: number | undefined, options?: {
2
+ defaultValue?: number;
3
+ label?: string;
4
+ }): number | undefined;
5
+ //# sourceMappingURL=byte-budget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"byte-budget.d.ts","sourceRoot":"","sources":["../src/byte-budget.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GACtD,MAAM,GAAG,SAAS,CASpB"}
@@ -0,0 +1,9 @@
1
+ export function normalizeMaxBytes(value, options = {}) {
2
+ const selected = value === undefined ? options.defaultValue : value;
3
+ if (selected === undefined || selected === Number.POSITIVE_INFINITY)
4
+ return selected;
5
+ if (!Number.isSafeInteger(selected) || selected < 0) {
6
+ throw new RangeError(`${options.label ?? "maxBytes"} must be a non-negative safe integer or Infinity`);
7
+ }
8
+ return selected;
9
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"file-hash.d.ts","sourceRoot":"","sources":["../src/file-hash.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAGnE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,UAAU,CAAC;AAElD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,MAAM,GAAE,aAAa,GAAG,SAA8B,GACrD,OAAO,CAAC,gBAAgB,CAAC,CAoB3B;AA2CD,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAElF"}
1
+ {"version":3,"file":"file-hash.d.ts","sourceRoot":"","sources":["../src/file-hash.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAInE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,UAAU,CAAC;AAElD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,MAAM,GAAE,aAAa,GAAG,SAA8B,GACrD,OAAO,CAAC,gBAAgB,CAAC,CAoB3B;AA+CD,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAElF"}
package/dist/file-hash.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import { FsSafeError } from "./errors.js";
4
- import { sameFileIdentity } from "./file-identity.js";
5
4
  import { getNativeBinding } from "./native.js";
6
5
  import { resolveReadOpenFlags } from "./read-open-flags.js";
6
+ import { inspectFileIdentity } from "./strict-file-identity.js";
7
7
  export async function hashFileHandle(handle, native = getNativeBinding()) {
8
8
  const stat = await handle.stat();
9
9
  if (!stat.isFile()) {
@@ -25,13 +25,16 @@ export async function hashFileHandle(handle, native = getNativeBinding()) {
25
25
  }
26
26
  }
27
27
  async function hashPath(filePath) {
28
- const before = await fs.lstat(filePath);
29
- if (before.isSymbolicLink()) {
30
- throw new FsSafeError("symlink", "SHA-256 path must not be a symbolic link");
31
- }
32
- if (!before.isFile()) {
33
- throw new FsSafeError("not-file", "SHA-256 path is not a regular file");
34
- }
28
+ const before = await inspectFileIdentity(async () => {
29
+ const stat = await fs.lstat(filePath, { bigint: true });
30
+ if (stat.isSymbolicLink()) {
31
+ throw new FsSafeError("symlink", "SHA-256 path must not be a symbolic link");
32
+ }
33
+ if (!stat.isFile()) {
34
+ throw new FsSafeError("not-file", "SHA-256 path is not a regular file");
35
+ }
36
+ return stat;
37
+ });
35
38
  let handle;
36
39
  try {
37
40
  handle = await fs.open(filePath, resolveReadOpenFlags());
@@ -45,17 +48,20 @@ async function hashPath(filePath) {
45
48
  throw error;
46
49
  }
47
50
  try {
48
- const opened = await handle.stat();
49
- const current = await fs.lstat(filePath);
50
- if (!opened.isFile()) {
51
- throw new FsSafeError("not-file", "SHA-256 path is not a regular file");
52
- }
53
- if (current.isSymbolicLink() ||
54
- !current.isFile() ||
55
- !sameFileIdentity(before, opened) ||
56
- !sameFileIdentity(opened, current)) {
57
- throw new FsSafeError("path-mismatch", "SHA-256 path changed while opening");
58
- }
51
+ const opened = await inspectFileIdentity(async () => {
52
+ const stat = await handle.stat({ bigint: true });
53
+ if (!stat.isFile()) {
54
+ throw new FsSafeError("not-file", "SHA-256 path is not a regular file");
55
+ }
56
+ return stat;
57
+ }, before);
58
+ await inspectFileIdentity(async () => {
59
+ const stat = await fs.lstat(filePath, { bigint: true });
60
+ if (stat.isSymbolicLink() || !stat.isFile()) {
61
+ throw new FsSafeError("path-mismatch", "SHA-256 path changed while opening");
62
+ }
63
+ return stat;
64
+ }, opened);
59
65
  return await hashFileHandle(handle);
60
66
  }
61
67
  finally {
@@ -1 +1 @@
1
- {"version":3,"file":"file-lock-sync.d.ts","sourceRoot":"","sources":["../src/file-lock-sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAOL,KAAK,wBAAwB,EAC9B,MAAM,2BAA2B,CAAC;AAKnC,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,yBAAyB,CAAC;AAGjC,MAAM,MAAM,0BAA0B,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,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,CAAC;IACxB,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,KAAK,CAAC;KAC1B,KAAK,OAAO,CAAC;IACd,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,wBAAwB,KAAK,OAAO,CAAC;IACxE,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,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,IAAI,OAAO,CAAC;IAC3B,OAAO,IAAI,IAAI,CAAC;IAChB,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AA0IF,wBAAgB,mBAAmB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,0BAA0B,CAAC,QAAQ,CAAC,GAC5C,kBAAkB,CAuJpB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,0BAA0B,CAAC,QAAQ,CAAC,EAC7C,EAAE,EAAE,MAAM,CAAC,GACV,CAAC,CAOH"}
1
+ {"version":3,"file":"file-lock-sync.d.ts","sourceRoot":"","sources":["../src/file-lock-sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAOL,KAAK,wBAAwB,EAC9B,MAAM,2BAA2B,CAAC;AAOnC,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,yBAAyB,CAAC;AAIjC,MAAM,MAAM,0BAA0B,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,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,CAAC;IACxB,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,KAAK,CAAC;KAC1B,KAAK,OAAO,CAAC;IACd,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,wBAAwB,KAAK,OAAO,CAAC;IACxE,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,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,IAAI,OAAO,CAAC;IAC3B,OAAO,IAAI,IAAI,CAAC;IAChB,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AA0IF,wBAAgB,mBAAmB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,0BAA0B,CAAC,QAAQ,CAAC,GAC5C,kBAAkB,CAwKpB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,0BAA0B,CAAC,QAAQ,CAAC,EAC7C,EAAE,EAAE,MAAM,CAAC,GACV,CAAC,CAOH"}
@@ -2,7 +2,8 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { FsSafeError } from "./errors.js";
4
4
  import { readSidecarLockSnapshotSync, relativeSidecarLockPath, removeSidecarLockIfUnchangedSync, serializeSidecarLockPayload, sidecarLockSnapshotMatches, } from "./sidecar-lock-reclaim.js";
5
- import { computeSidecarLockDelayMs, sidecarLockPayloadCreatedAtMs, } from "./sidecar-lock-policy.js";
5
+ import { computeSidecarLockDelayMs, sidecarLockPayloadCreatedAtMs, validateSidecarLockRetryOptions, validateSidecarLockTimeoutMs, } from "./sidecar-lock-policy.js";
6
+ import { getFsSafeLockConfig } from "./lock-config.js";
6
7
  import { sleepSync } from "./timing.js";
7
8
  const SYNC_HELD_LOCKS_KEY = Symbol.for("fsSafe.syncSidecarLocks");
8
9
  const SYNC_CLEANUP_REGISTERED_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupRegistered");
@@ -126,6 +127,11 @@ function reclaimGuardExists(reclaimGuardPath) {
126
127
  }
127
128
  }
128
129
  export function acquireFileLockSync(targetPath, options) {
130
+ const defaults = getFsSafeLockConfig();
131
+ const retry = options.retry ?? defaults.retry ?? {};
132
+ const timeoutMs = options.timeoutMs ?? defaults.timeoutMs;
133
+ validateSidecarLockRetryOptions(retry);
134
+ validateSidecarLockTimeoutMs(timeoutMs);
129
135
  const normalizedTargetPath = normalizeTargetPath(targetPath);
130
136
  const lockPath = boundedLockPath(options.lockPath ?? `${normalizedTargetPath}.lock`, options.lockRoot);
131
137
  const heldLocks = getSyncHeldLocks();
@@ -137,14 +143,16 @@ export function acquireFileLockSync(targetPath, options) {
137
143
  held.refCount += 1;
138
144
  return createSyncHeldLockHandle(held);
139
145
  }
140
- const staleMs = options.staleMs ?? 30_000;
141
- const retry = options.retry ?? {};
146
+ // Process defaults fill the same fields here as withLockDefaults() fills for
147
+ // the asynchronous manager, so both acquirers honor configureFsSafeLocks().
148
+ const staleMs = options.staleMs ?? defaults.staleMs ?? 30_000;
149
+ const staleRecovery = options.staleRecovery ?? defaults.staleRecovery;
142
150
  const startedAt = Date.now();
143
151
  let attempt = 0;
144
152
  const reclaimGuardPath = `${lockPath}.reclaim`;
145
153
  const waitForRetry = () => {
146
154
  const elapsed = Date.now() - startedAt;
147
- const timedOut = options.timeoutMs !== undefined && elapsed >= options.timeoutMs;
155
+ const timedOut = timeoutMs !== undefined && elapsed >= timeoutMs;
148
156
  if (timedOut || (retry.retries !== undefined && attempt >= retry.retries)) {
149
157
  throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
150
158
  code: "file_lock_timeout",
@@ -152,7 +160,10 @@ export function acquireFileLockSync(targetPath, options) {
152
160
  normalizedTargetPath,
153
161
  });
154
162
  }
155
- sleepSync(computeSidecarLockDelayMs(retry, attempt));
163
+ const remaining = timeoutMs === undefined || timeoutMs === Number.POSITIVE_INFINITY
164
+ ? Number.POSITIVE_INFINITY
165
+ : Math.max(0, timeoutMs - elapsed);
166
+ sleepSync(Math.min(computeSidecarLockDelayMs(retry, attempt), remaining));
156
167
  attempt += 1;
157
168
  };
158
169
  while (true) {
@@ -190,9 +201,15 @@ export function acquireFileLockSync(targetPath, options) {
190
201
  const returnedHandle = createSyncHeldLockHandle(createdHeld);
191
202
  if (options.onCompromised && (options.compromiseCheckIntervalMs ?? 0) > 0) {
192
203
  createdHeld.timer = setInterval(() => {
193
- if (!returnedHandle.verifyStillHeld()) {
194
- if (createdHeld.timer)
195
- clearInterval(createdHeld.timer);
204
+ let stillHeld;
205
+ try {
206
+ stillHeld = returnedHandle.verifyStillHeld();
207
+ }
208
+ catch {
209
+ stillHeld = false;
210
+ }
211
+ if (!stillHeld && createdHeld.timer) {
212
+ clearInterval(createdHeld.timer);
196
213
  createdHeld.timer = undefined;
197
214
  options.onCompromised?.({ lockPath, normalizedTargetPath });
198
215
  }
@@ -232,7 +249,7 @@ export function acquireFileLockSync(targetPath, options) {
232
249
  })
233
250
  : defaultShouldReclaim(snapshot.payload, lockPath, staleMs, nowMs);
234
251
  if (reclaim) {
235
- if (options.staleRecovery === "remove-if-unchanged" &&
252
+ if (staleRecovery === "remove-if-unchanged" &&
236
253
  snapshot.raw !== undefined &&
237
254
  options.shouldRemoveStaleLock?.({
238
255
  lockPath,
@@ -1 +1 @@
1
- {"version":3,"file":"file-store-boundary.d.ts","sourceRoot":"","sources":["../src/file-store-boundary.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAA4C,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGhF,MAAM,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAOjD,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,IAAI,EAChB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AA+CD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAuC1D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAWrE;AAUD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,eAAe,CAOlB;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,eAAe,GAAG,OAAO,CAAC;CAC1C,GAAG,eAAe,CAyDlB"}
1
+ {"version":3,"file":"file-store-boundary.d.ts","sourceRoot":"","sources":["../src/file-store-boundary.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGvD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAA4C,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGhF,MAAM,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAOjD,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,IAAI,EAChB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhB;AAgDD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAwC1D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAWrE;AAUD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,eAAe,CAOlB;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,eAAe,GAAG,OAAO,CAAC;CAC1C,GAAG,eAAe,CAyDlB"}
@@ -2,6 +2,7 @@ import syncFs from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { Transform } from "node:stream";
5
+ import { normalizeMaxBytes } from "./byte-budget.js";
5
6
  import { pipeline } from "node:stream/promises";
6
7
  import { assertSyncDirectoryGuard as assertDirectoryGuardSync, createSyncDirectoryGuard, } from "./directory-guard.js";
7
8
  import { FsSafeError } from "./errors.js";
@@ -22,9 +23,10 @@ export async function ensureParentInRoot(scopedRoot, relativePath, mode) {
22
23
  await chmodDirectoryInRootBestEffort(scopedRoot, parent, mode).catch(() => undefined);
23
24
  }
24
25
  export async function openWritableStoreRoot(params) {
26
+ const maxBytes = normalizeMaxBytes(params.maxBytes);
25
27
  await fs.mkdir(params.rootDir, { recursive: true, mode: params.dirMode });
26
28
  await fs.chmod(params.rootDir, params.dirMode).catch(() => undefined);
27
- return await root(params.rootDir, { hardlinks: "reject", maxBytes: params.maxBytes });
29
+ return await root(params.rootDir, { hardlinks: "reject", maxBytes });
28
30
  }
29
31
  async function chmodDirectoryInRootBestEffort(scopedRoot, relativePath, mode) {
30
32
  const dirPath = await scopedRoot.resolve(relativePath);
@@ -49,7 +51,8 @@ async function chmodDirectoryInRootBestEffort(scopedRoot, relativePath, mode) {
49
51
  }
50
52
  }
51
53
  function createMaxBytesTransform(maxBytes) {
52
- if (maxBytes === undefined) {
54
+ const limit = normalizeMaxBytes(maxBytes);
55
+ if (limit === undefined) {
53
56
  return undefined;
54
57
  }
55
58
  let total = 0;
@@ -57,8 +60,8 @@ function createMaxBytesTransform(maxBytes) {
57
60
  transform(chunk, _encoding, callback) {
58
61
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
59
62
  total += buffer.byteLength;
60
- if (total > maxBytes) {
61
- callback(new FsSafeError("too-large", `file exceeds maximum size of ${maxBytes} bytes`));
63
+ if (total > limit) {
64
+ callback(new FsSafeError("too-large", `file exceeds maximum size of ${limit} bytes`));
62
65
  return;
63
66
  }
64
67
  callback(null, buffer);
@@ -66,6 +69,7 @@ function createMaxBytesTransform(maxBytes) {
66
69
  });
67
70
  }
68
71
  export async function writeStreamToTempSource(params) {
72
+ const maxBytes = normalizeMaxBytes(params.maxBytes);
69
73
  const tempRoot = resolveSecureTempRoot({
70
74
  fallbackPrefix: "fs-safe-file-store",
71
75
  unsafeFallbackLabel: "file store temp dir",
@@ -81,7 +85,7 @@ export async function writeStreamToTempSource(params) {
81
85
  writable.once("close", () => {
82
86
  handleClosedByStream = true;
83
87
  });
84
- const limiter = createMaxBytesTransform(params.maxBytes);
88
+ const limiter = createMaxBytesTransform(maxBytes);
85
89
  if (limiter) {
86
90
  await pipeline(params.stream, limiter, writable);
87
91
  }
@@ -0,0 +1,2 @@
1
+ export declare function assertFileStoreMaxBytes(size: number, maxBytes?: number): void;
2
+ //# sourceMappingURL=file-store-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-store-limit.d.ts","sourceRoot":"","sources":["../src/file-store-limit.ts"],"names":[],"mappings":"AAGA,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAK7E"}
@@ -0,0 +1,8 @@
1
+ import { normalizeMaxBytes } from "./byte-budget.js";
2
+ import { FsSafeError } from "./errors.js";
3
+ export function assertFileStoreMaxBytes(size, maxBytes) {
4
+ const limit = normalizeMaxBytes(maxBytes);
5
+ if (limit !== undefined && size > limit) {
6
+ throw new FsSafeError("too-large", `file exceeds maximum size of ${limit} bytes`);
7
+ }
8
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"file-store-sync-write.d.ts","sourceRoot":"","sources":["../src/file-store-sync-write.ts"],"names":[],"mappings":"AAuBA,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CA+ET"}
1
+ {"version":3,"file":"file-store-sync-write.d.ts","sourceRoot":"","sources":["../src/file-store-sync-write.ts"],"names":[],"mappings":"AAwBA,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CA6GT"}