@openclaw/fs-safe 0.8.2 → 0.8.4

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 (200) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +1 -1
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +8 -7
  5. package/dist/advanced.d.ts +2 -2
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +2 -2
  8. package/dist/archive-kind.d.ts +1 -0
  9. package/dist/archive-kind.d.ts.map +1 -1
  10. package/dist/archive-kind.js +6 -0
  11. package/dist/archive-limits.d.ts +0 -1
  12. package/dist/archive-limits.d.ts.map +1 -1
  13. package/dist/archive-limits.js +0 -3
  14. package/dist/archive-native.d.ts +1 -0
  15. package/dist/archive-native.d.ts.map +1 -1
  16. package/dist/archive-native.js +20 -76
  17. package/dist/archive-parser-errors.d.ts +2 -0
  18. package/dist/archive-parser-errors.d.ts.map +1 -0
  19. package/dist/archive-parser-errors.js +12 -0
  20. package/dist/archive-plan.d.ts +20 -0
  21. package/dist/archive-plan.d.ts.map +1 -0
  22. package/dist/archive-plan.js +55 -0
  23. package/dist/archive-read.d.ts.map +1 -1
  24. package/dist/archive-read.js +7 -13
  25. package/dist/archive-tar-inspect.d.ts +7 -0
  26. package/dist/archive-tar-inspect.d.ts.map +1 -0
  27. package/dist/archive-tar-inspect.js +59 -0
  28. package/dist/archive-tar-wasm.d.ts +1 -1
  29. package/dist/archive-tar-wasm.d.ts.map +1 -1
  30. package/dist/archive-tar-wasm.js +5 -8
  31. package/dist/archive-tar.d.ts +3 -1
  32. package/dist/archive-tar.d.ts.map +1 -1
  33. package/dist/archive-tar.js +12 -49
  34. package/dist/archive.d.ts +1 -0
  35. package/dist/archive.d.ts.map +1 -1
  36. package/dist/archive.js +54 -33
  37. package/dist/atomic.d.ts +1 -1
  38. package/dist/atomic.d.ts.map +1 -1
  39. package/dist/atomic.js +1 -1
  40. package/dist/bounded-read-stream.d.ts +1 -5
  41. package/dist/bounded-read-stream.d.ts.map +1 -1
  42. package/dist/bounded-read-stream.js +5 -13
  43. package/dist/bounded-read.d.ts.map +1 -1
  44. package/dist/bounded-read.js +31 -4
  45. package/dist/byte-budget.d.ts +0 -1
  46. package/dist/byte-budget.d.ts.map +1 -1
  47. package/dist/byte-budget.js +1 -1
  48. package/dist/directory-durability.js +5 -5
  49. package/dist/directory-guard.d.ts.map +1 -1
  50. package/dist/directory-guard.js +5 -6
  51. package/dist/directory-mode-owner.d.ts.map +1 -1
  52. package/dist/directory-mode-owner.js +2 -0
  53. package/dist/error-detail.d.ts +1 -0
  54. package/dist/error-detail.d.ts.map +1 -1
  55. package/dist/error-detail.js +6 -0
  56. package/dist/file-store-boundary.d.ts +1 -1
  57. package/dist/file-store-boundary.d.ts.map +1 -1
  58. package/dist/file-store-boundary.js +3 -14
  59. package/dist/file-store-prune.js +17 -8
  60. package/dist/file-store.js +3 -3
  61. package/dist/file-sync.d.ts +5 -0
  62. package/dist/file-sync.d.ts.map +1 -0
  63. package/dist/file-sync.js +19 -0
  64. package/dist/guarded-mkdir.d.ts.map +1 -1
  65. package/dist/guarded-mkdir.js +5 -4
  66. package/dist/guarded-mutation.d.ts +3 -0
  67. package/dist/guarded-mutation.d.ts.map +1 -1
  68. package/dist/guarded-mutation.js +5 -1
  69. package/dist/home-dir.d.ts +0 -10
  70. package/dist/home-dir.d.ts.map +1 -1
  71. package/dist/home-dir.js +0 -28
  72. package/dist/json-durable-queue-ownership.d.ts +1 -0
  73. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  74. package/dist/json-durable-queue-ownership.js +139 -18
  75. package/dist/json-durable-queue.d.ts.map +1 -1
  76. package/dist/json-durable-queue.js +5 -10
  77. package/dist/json-store.d.ts.map +1 -1
  78. package/dist/json-store.js +1 -1
  79. package/dist/local-roots.d.ts.map +1 -1
  80. package/dist/local-roots.js +2 -3
  81. package/dist/move-path-cleanup.d.ts +1 -1
  82. package/dist/move-path-cleanup.d.ts.map +1 -1
  83. package/dist/move-path-cleanup.js +4 -2
  84. package/dist/move-path.d.ts +9 -0
  85. package/dist/move-path.d.ts.map +1 -1
  86. package/dist/move-path.js +53 -16
  87. package/dist/native-binding.d.ts +2 -1
  88. package/dist/native-binding.d.ts.map +1 -1
  89. package/dist/native-operations.d.ts +0 -1
  90. package/dist/native-operations.d.ts.map +1 -1
  91. package/dist/native-operations.js +0 -10
  92. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  93. package/dist/native-pinned-write-windows.js +8 -4
  94. package/dist/native-pinned-write.js +3 -3
  95. package/dist/native-staged-file.d.ts +10 -3
  96. package/dist/native-staged-file.d.ts.map +1 -1
  97. package/dist/native-staged-file.js +38 -13
  98. package/dist/opened-realpath.d.ts +12 -1
  99. package/dist/opened-realpath.d.ts.map +1 -1
  100. package/dist/opened-realpath.js +16 -14
  101. package/dist/output.d.ts.map +1 -1
  102. package/dist/output.js +26 -2
  103. package/dist/path-policy.js +3 -3
  104. package/dist/permissions.d.ts +1 -0
  105. package/dist/permissions.d.ts.map +1 -1
  106. package/dist/permissions.js +3 -0
  107. package/dist/pinned-write.d.ts +2 -2
  108. package/dist/pinned-write.d.ts.map +1 -1
  109. package/dist/pinned-write.js +17 -22
  110. package/dist/private-temp-workspace.js +1 -1
  111. package/dist/publish-file.js +2 -2
  112. package/dist/regular-file.js +8 -8
  113. package/dist/replace-file-copy-source.d.ts.map +1 -1
  114. package/dist/replace-file-copy-source.js +2 -7
  115. package/dist/replace-file-descriptor.d.ts.map +1 -1
  116. package/dist/replace-file-descriptor.js +11 -20
  117. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  118. package/dist/replace-file-temp-owner.js +15 -14
  119. package/dist/replace-file.d.ts.map +1 -1
  120. package/dist/replace-file.js +21 -8
  121. package/dist/root-context.js +5 -5
  122. package/dist/root-errors.d.ts +1 -0
  123. package/dist/root-errors.d.ts.map +1 -1
  124. package/dist/root-errors.js +9 -0
  125. package/dist/root-file.d.ts.map +1 -1
  126. package/dist/root-file.js +17 -28
  127. package/dist/root-impl.d.ts +2 -1
  128. package/dist/root-impl.d.ts.map +1 -1
  129. package/dist/root-impl.js +46 -58
  130. package/dist/root-path-existing.d.ts.map +1 -1
  131. package/dist/root-path-existing.js +2 -3
  132. package/dist/root-path-symlink.d.ts.map +1 -1
  133. package/dist/root-path-symlink.js +2 -3
  134. package/dist/root-path.d.ts.map +1 -1
  135. package/dist/root-path.js +51 -97
  136. package/dist/root-paths.d.ts.map +1 -1
  137. package/dist/root-paths.js +16 -15
  138. package/dist/root-write-mode.d.ts +6 -0
  139. package/dist/root-write-mode.d.ts.map +1 -0
  140. package/dist/root-write-mode.js +61 -0
  141. package/dist/root-write-verification.js +8 -8
  142. package/dist/safe-path-segment.d.ts +1 -0
  143. package/dist/safe-path-segment.d.ts.map +1 -1
  144. package/dist/safe-path-segment.js +1 -1
  145. package/dist/secret-file.d.ts.map +1 -1
  146. package/dist/secret-file.js +4 -11
  147. package/dist/secret-read-async.d.ts.map +1 -1
  148. package/dist/secret-read-async.js +2 -10
  149. package/dist/secret-read-policy.d.ts +4 -0
  150. package/dist/secret-read-policy.d.ts.map +1 -1
  151. package/dist/secret-read-policy.js +12 -0
  152. package/dist/sibling-staged-file.d.ts.map +1 -1
  153. package/dist/sibling-staged-file.js +2 -7
  154. package/dist/sidecar-lock-policy.d.ts +0 -1
  155. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  156. package/dist/sidecar-lock-policy.js +0 -4
  157. package/dist/strict-file-identity.d.ts +1 -1
  158. package/dist/strict-file-identity.d.ts.map +1 -1
  159. package/dist/string-coerce.d.ts +0 -9
  160. package/dist/string-coerce.d.ts.map +1 -1
  161. package/dist/string-coerce.js +0 -56
  162. package/dist/symlink-parents.d.ts.map +1 -1
  163. package/dist/symlink-parents.js +1 -2
  164. package/dist/temp-target.d.ts.map +1 -1
  165. package/dist/temp-target.js +1 -12
  166. package/docs/archive.md +57 -0
  167. package/docs/atomic.md +53 -19
  168. package/docs/reading.md +2 -0
  169. package/docs/root.md +10 -1
  170. package/docs/staged-file.md +6 -2
  171. package/docs/types.md +2 -1
  172. package/docs/writing.md +30 -2
  173. package/package.json +8 -8
  174. package/dist/archive-tar-extract.d.ts +0 -11
  175. package/dist/archive-tar-extract.d.ts.map +0 -1
  176. package/dist/archive-tar-extract.js +0 -49
  177. package/dist/fsync.d.ts +0 -2
  178. package/dist/fsync.d.ts.map +0 -1
  179. package/dist/fsync.js +0 -1
  180. package/dist/json-durable-queue-retirement.d.ts +0 -9
  181. package/dist/json-durable-queue-retirement.d.ts.map +0 -1
  182. package/dist/json-durable-queue-retirement.js +0 -126
  183. package/dist/json-durable-queue-transfer-lock.d.ts +0 -2
  184. package/dist/json-durable-queue-transfer-lock.d.ts.map +0 -1
  185. package/dist/json-durable-queue-transfer-lock.js +0 -19
  186. package/dist/mode.d.ts +0 -2
  187. package/dist/mode.d.ts.map +0 -1
  188. package/dist/mode.js +0 -3
  189. package/dist/output-sibling.d.ts +0 -8
  190. package/dist/output-sibling.d.ts.map +0 -1
  191. package/dist/output-sibling.js +0 -30
  192. package/dist/read-error.d.ts +0 -2
  193. package/dist/read-error.d.ts.map +0 -1
  194. package/dist/read-error.js +0 -11
  195. package/dist/short-path.d.ts +0 -2
  196. package/dist/short-path.d.ts.map +0 -1
  197. package/dist/short-path.js +0 -7
  198. package/dist/staged-file.d.ts +0 -10
  199. package/dist/staged-file.d.ts.map +0 -1
  200. package/dist/staged-file.js +0 -15
@@ -1,3 +1,4 @@
1
+ import { syncFileBestEffort } from "./file-sync.js";
1
2
  import fsSync, {} from "node:fs";
2
3
  import fs, {} from "node:fs/promises";
3
4
  import path from "node:path";
@@ -92,13 +93,7 @@ export async function writeCallbackSibling(params) {
92
93
  }
93
94
  if (params.syncTempFile) {
94
95
  await assertCurrent(params.tempPath);
95
- try {
96
- await handle.sync();
97
- }
98
- catch (error) {
99
- if (error?.code !== "EPERM")
100
- throw error;
101
- }
96
+ await syncFileBestEffort(handle);
102
97
  }
103
98
  await assertCurrent(params.tempPath);
104
99
  await fs.rename(params.tempPath, filePath);
@@ -4,7 +4,6 @@ export declare function validateSidecarLockTimeoutMs(timeoutMs: number | undefin
4
4
  export declare function computeSidecarLockDelayMs(retry: SidecarLockRetryOptions, attempt: number): number;
5
5
  export declare const maxTransientLockDenials = 8;
6
6
  export declare function isTransientLockFileDenial(error: unknown, lockPath: string): boolean;
7
- export declare function sidecarLockPayloadIsStale(payload: unknown, staleMs: number, nowMs: number): boolean;
8
7
  export declare function sidecarLockPayloadCreatedAtMs(payload: unknown): number | null;
9
8
  export declare function defaultSidecarLockShouldReclaim(params: {
10
9
  lockPath: string;
@@ -1 +1 @@
1
- {"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAQvE,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAkBpF;AAED,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGhF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAQjG;AASD,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGnF;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAGT;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAU7E;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,OAAO,CAAC,CAQnB"}
1
+ {"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAQvE,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAkBpF;AAED,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGhF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAQjG;AASD,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGnF;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAU7E;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,OAAO,CAAC,CAQnB"}
@@ -48,10 +48,6 @@ export function isTransientLockFileDenial(error, lockPath) {
48
48
  const denial = error;
49
49
  return process.platform === "win32" && denial?.code === "EPERM" && denial.path === lockPath;
50
50
  }
51
- export function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
52
- const createdAtMs = sidecarLockPayloadCreatedAtMs(payload);
53
- return createdAtMs !== null && nowMs - createdAtMs > staleMs;
54
- }
55
51
  export function sidecarLockPayloadCreatedAtMs(payload) {
56
52
  const createdAt = payload &&
57
53
  typeof payload === "object" &&
@@ -1,6 +1,6 @@
1
1
  import type { BigIntStats } from "node:fs";
2
2
  type ExactFileIdentity = Pick<BigIntStats, "dev" | "ino">;
3
- export declare function inspectFileIdentity<T extends ExactFileIdentity>(inspect: () => Promise<T>, expected?: ExactFileIdentity, platform?: NodeJS.Platform): Promise<T>;
3
+ export declare function inspectFileIdentity<T extends ExactFileIdentity>(inspect: () => T | Promise<T>, expected?: ExactFileIdentity, platform?: NodeJS.Platform): Promise<T>;
4
4
  export declare function inspectFileIdentitySync<T extends ExactFileIdentity>(inspect: () => T, expected?: ExactFileIdentity, platform?: NodeJS.Platform): T;
5
5
  export {};
6
6
  //# sourceMappingURL=strict-file-identity.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"strict-file-identity.d.ts","sourceRoot":"","sources":["../src/strict-file-identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAI3C,KAAK,iBAAiB,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC;AA+B1D,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,iBAAiB,EACnE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,iBAAiB,EACjE,OAAO,EAAE,MAAM,CAAC,EAChB,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,CAAC,CAOH"}
1
+ {"version":3,"file":"strict-file-identity.d.ts","sourceRoot":"","sources":["../src/strict-file-identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAI3C,KAAK,iBAAiB,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC;AA+B1D,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,iBAAiB,EACnE,OAAO,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAC7B,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,iBAAiB,EACjE,OAAO,EAAE,MAAM,CAAC,EAChB,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,CAAC,CAOH"}
@@ -1,14 +1,5 @@
1
- export declare function readStringValue(value: unknown): string | undefined;
2
1
  export declare function normalizeNullableString(value: unknown): string | null;
3
2
  export declare function normalizeOptionalString(value: unknown): string | undefined;
4
- export declare function normalizeStringifiedOptionalString(value: unknown): string | undefined;
5
3
  export declare function normalizeOptionalLowercaseString(value: unknown): string | undefined;
6
4
  export declare function normalizeLowercaseStringOrEmpty(value: unknown): string;
7
- export declare function normalizeFastMode(raw?: string | boolean | null): boolean | undefined;
8
- export declare function lowercasePreservingWhitespace(value: string): string;
9
- export declare function localeLowercasePreservingWhitespace(value: string): string;
10
- export declare function resolvePrimaryStringValue(value: unknown): string | undefined;
11
- export declare function normalizeOptionalThreadValue(value: unknown): string | number | undefined;
12
- export declare function normalizeOptionalStringifiedId(value: unknown): string | undefined;
13
- export declare function hasNonEmptyString(value: unknown): value is string;
14
5
  //# sourceMappingURL=string-coerce.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"string-coerce.d.ts","sourceRoot":"","sources":["../src/string-coerce.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAElE;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAMrE;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE1E;AAED,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAQrF;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAEnF;AAED,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtE;AAED,wBAAgB,iBAAiB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,OAAO,GAAG,SAAS,CAepF;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED,wBAAgB,mCAAmC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAQ5E;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAKxF;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAGjF;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEjE"}
1
+ {"version":3,"file":"string-coerce.d.ts","sourceRoot":"","sources":["../src/string-coerce.ts"],"names":[],"mappings":"AAAA,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAMrE;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE1E;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAEnF;AAED,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtE"}
@@ -1,6 +1,3 @@
1
- export function readStringValue(value) {
2
- return typeof value === "string" ? value : undefined;
3
- }
4
1
  export function normalizeNullableString(value) {
5
2
  if (typeof value !== "string") {
6
3
  return null;
@@ -11,62 +8,9 @@ export function normalizeNullableString(value) {
11
8
  export function normalizeOptionalString(value) {
12
9
  return normalizeNullableString(value) ?? undefined;
13
10
  }
14
- export function normalizeStringifiedOptionalString(value) {
15
- if (typeof value === "string") {
16
- return normalizeOptionalString(value);
17
- }
18
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
19
- return normalizeOptionalString(String(value));
20
- }
21
- return undefined;
22
- }
23
11
  export function normalizeOptionalLowercaseString(value) {
24
12
  return normalizeOptionalString(value)?.toLowerCase();
25
13
  }
26
14
  export function normalizeLowercaseStringOrEmpty(value) {
27
15
  return normalizeOptionalLowercaseString(value) ?? "";
28
16
  }
29
- export function normalizeFastMode(raw) {
30
- if (typeof raw === "boolean") {
31
- return raw;
32
- }
33
- if (!raw) {
34
- return undefined;
35
- }
36
- const key = normalizeLowercaseStringOrEmpty(raw);
37
- if (["off", "false", "no", "0", "disable", "disabled", "normal"].includes(key)) {
38
- return false;
39
- }
40
- if (["on", "true", "yes", "1", "enable", "enabled", "fast"].includes(key)) {
41
- return true;
42
- }
43
- return undefined;
44
- }
45
- export function lowercasePreservingWhitespace(value) {
46
- return value.toLowerCase();
47
- }
48
- export function localeLowercasePreservingWhitespace(value) {
49
- return value.toLocaleLowerCase();
50
- }
51
- export function resolvePrimaryStringValue(value) {
52
- if (typeof value === "string") {
53
- return normalizeOptionalString(value);
54
- }
55
- if (!value || typeof value !== "object") {
56
- return undefined;
57
- }
58
- return normalizeOptionalString(value.primary);
59
- }
60
- export function normalizeOptionalThreadValue(value) {
61
- if (typeof value === "number") {
62
- return Number.isFinite(value) ? Math.trunc(value) : undefined;
63
- }
64
- return normalizeOptionalString(value);
65
- }
66
- export function normalizeOptionalStringifiedId(value) {
67
- const normalized = normalizeOptionalThreadValue(value);
68
- return normalized == null ? undefined : String(normalized);
69
- }
70
- export function hasNonEmptyString(value) {
71
- return normalizeOptionalString(value) !== undefined;
72
- }
@@ -1 +1 @@
1
- {"version":3,"file":"symlink-parents.d.ts","sourceRoot":"","sources":["../src/symlink-parents.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAyBF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,6BAA6B,GACpC,IAAI,CA6BN"}
1
+ {"version":3,"file":"symlink-parents.d.ts","sourceRoot":"","sources":["../src/symlink-parents.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAyBF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,6BAA6B,GACpC,IAAI,CA6BN"}
@@ -1,5 +1,4 @@
1
1
  import fsSync from "node:fs";
2
- import fs from "node:fs/promises";
3
2
  import path from "node:path";
4
3
  import { FsSafeError } from "./errors.js";
5
4
  import { hasNodeErrorCode, isPathRelativeEscape } from "./path.js";
@@ -30,7 +29,7 @@ export async function assertNoSymlinkParents(params) {
30
29
  for (const [index, segment] of walk.segments.entries()) {
31
30
  current = path.join(current, segment);
32
31
  try {
33
- const stat = await fs.lstat(current);
32
+ const stat = fsSync.lstatSync(current);
34
33
  if (stat.isSymbolicLink()) {
35
34
  if (params.allowRootChildSymlink && path.dirname(current) === walk.root) {
36
35
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AA0EF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAaT;AAsCD,wBAAsB,QAAQ,CAAC,MAAM,EAAE;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,GAAG,OAAO,CAAC,QAAQ,CAAC,CAwBpB;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE;IACN,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,EACD,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
1
+ {"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AA8DF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAaT;AAsCD,wBAAsB,QAAQ,CAAC,MAAM,EAAE;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,GAAG,OAAO,CAAC,QAAQ,CAAC,CAwBpB;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE;IACN,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,EACD,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { sameFileIdentityForCleanup } from "./file-identity.js";
5
- import { assertSafePathSegment, sanitizeSafePathSegment } from "./safe-path-segment.js";
5
+ import { assertSafePathSegment, sanitizeSafePathSegment, trimHyphenEdges } from "./safe-path-segment.js";
6
6
  import { resolveSecureTempRoot } from "./secure-temp-dir.js";
7
7
  import { registerTempPathForExit } from "./temp-cleanup.js";
8
8
  const HYPHEN_CHAR_CODE = 0x2d;
@@ -14,17 +14,6 @@ const UPPERCASE_Z_CHAR_CODE = 0x5a;
14
14
  const UNDERSCORE_CHAR_CODE = 0x5f;
15
15
  const LOWERCASE_A_CHAR_CODE = 0x61;
16
16
  const LOWERCASE_Z_CHAR_CODE = 0x7a;
17
- function trimHyphenEdges(value) {
18
- let start = 0;
19
- let end = value.length;
20
- while (start < end && value.charCodeAt(start) === HYPHEN_CHAR_CODE) {
21
- start += 1;
22
- }
23
- while (end > start && value.charCodeAt(end - 1) === HYPHEN_CHAR_CODE) {
24
- end -= 1;
25
- }
26
- return start === 0 && end === value.length ? value : value.slice(start, end);
27
- }
28
17
  function isExtensionCharCode(charCode) {
29
18
  return ((charCode >= NUMBER_ZERO_CHAR_CODE && charCode <= NUMBER_NINE_CHAR_CODE) ||
30
19
  (charCode >= UPPERCASE_A_CHAR_CODE && charCode <= UPPERCASE_Z_CHAR_CODE) ||
package/docs/archive.md CHANGED
@@ -422,6 +422,63 @@ Normal link/filter policy still governs the described member. Canonical
422
422
  pre-strip filter paths, decoded-stream ceilings, and physical EOF checks apply
423
423
  to plain/gzip TAR and native zstd/bzip2 alike.
424
424
 
425
+ ## `inspectTarArchive`
426
+
427
+ Inspect accepted TAR members without creating an extracted tree. This operation
428
+ uses the same complete Rust/WASM admission and TypeScript extraction planner as
429
+ `extractArchive`, with zero stripping. It detects plain TAR or gzip from the
430
+ input bytes; ZIP, zstd, and bzip2 are not part of this inspection API.
431
+
432
+ ```ts
433
+ import { inspectTarArchive } from "@openclaw/fs-safe/archive";
434
+
435
+ const entries = await inspectTarArchive({
436
+ archivePath: "/srv/uploads/tree.tar.gz",
437
+ timeoutMs: 30_000,
438
+ limits: {
439
+ maxArchiveBytes: 16 * 1024 * 1024,
440
+ maxEntries: 5_000,
441
+ maxEntryBytes: 16 * 1024 * 1024,
442
+ maxExtractedBytes: 64 * 1024 * 1024,
443
+ },
444
+ entryFilter: ({ kind }) => kind === "file" || kind === "directory" ? "extract" : "skip",
445
+ onFiltered: "reject-archive",
446
+ });
447
+ ```
448
+
449
+ `InspectTarArchiveOptions` accepts `archivePath`, `timeoutMs`, `limits`,
450
+ `entryFilter`, and `onFiltered`, with the same defaults and error classes as
451
+ extraction. The result is a frozen array of frozen `InspectedTarEntry` records:
452
+ `{ path: string; kind: "file" | "directory"; size: number }`, in archive order.
453
+ `size` is the effective declared payload length. `path` is extraction's
454
+ canonical pre-strip identity: case, Unicode spelling, BOM, and embedded LF are
455
+ preserved, while separators and dot components follow the existing archive path
456
+ contract. No human-readable tar listing or escape decoding is involved.
457
+
458
+ Full framing, gzip integrity, EOF, metadata, decoded-byte, and manifest-budget
459
+ validation finishes before the caller's filter runs. The shared planner then
460
+ applies traversal, collision, depth, blocked-type, and accepted-payload limits.
461
+ A failure returns no partial result. Filter callbacks are decisions, not admission
462
+ receipts: later policy, collision, or budget checks can still reject the archive.
463
+ Only the resolved Promise/result is authorization-worthy; do not perform
464
+ irreversible actions from a filter callback.
465
+
466
+ Root-only records count toward entry limits but produce no result; PAX/GNU metadata headers are not members. Only accepted
467
+ file/directory members appear, not implicit parent directories. Unsupported
468
+ records follow extraction's omission policy unless the filter rejects them, as
469
+ in the example. AppleDouble records encoded as ordinary files are ordinary
470
+ members, not hidden metadata.
471
+
472
+ Inspection pins and privately stages its input, then cleans up that copy. It
473
+ neither creates destination paths nor tests destination permissions or platform
474
+ filename restrictions. Its result is evidence about those inspected bytes, not
475
+ an extraction capability or a promise that a later file at `archivePath` is
476
+ unchanged. Callers making authorization decisions must retain the same private
477
+ immutable archive or verify byte identity before extracting with matching
478
+ filter/limit settings. Extraction always performs its own admission and guarded
479
+ publication. Native `off`, `auto`, and `require` retain their existing selection
480
+ and availability semantics; inspection does not fall back after native failure.
481
+
425
482
  ## `resolveArchiveKind`
426
483
 
427
484
  ```ts
package/docs/atomic.md CHANGED
@@ -173,7 +173,8 @@ metadata where lower latency matters more than crash-durability.
173
173
 
174
174
  ## `movePathWithCopyFallback`
175
175
 
176
- Rename a path. If the rename fails with `EXDEV` (cross-device), fall back to
176
+ Rename a path. If the rename fails with `EXDEV` (cross-device), or `EPERM` on
177
+ Windows, fall back to
177
178
  copying into a staged sibling path, renaming that staged path into place, and
178
179
  then removing only the source entries that were copied. The fallback avoids
179
180
  buffering regular files into memory and does not tighten the destination parent
@@ -215,39 +216,72 @@ identity becomes the next cleanup receipt. This accounts for the operation's
215
216
  own link-count and ctime changes without suppressing unexpected external
216
217
  mutations.
217
218
 
218
- ### Final rename authorization
219
+ ### Mutation authority and publication receipts
219
220
 
220
- Pass `assertBeforeRename` when a move depends on a revocable lease or another
221
- caller-owned authorization. The helper captures this callback when called and
222
- runs it synchronously after asynchronous preparation and directory checks,
223
- immediately before each rename is dispatched:
221
+ Pass `assertBeforeMutation` when a move depends on a revocable lease or another
222
+ caller-owned authorization. The helper captures the callback when called and
223
+ runs it synchronously after asynchronous preparation and identity checks,
224
+ immediately before each rename, source-file or source-symlink unlink, and
225
+ source-directory removal. `assertBeforeRename` remains available for callers
226
+ that only guard publication. When both are set, the rename check runs first,
227
+ then the mutation check, without yielding before dispatch.
224
228
 
225
229
  ```ts
230
+ type MovePathPublicationReceipt = Readonly<{
231
+ path: string;
232
+ dev: bigint;
233
+ ino: bigint;
234
+ }>;
235
+
226
236
  type MovePathWithCopyFallbackOptions = {
227
237
  from: string;
228
238
  sourceHardlinks?: "allow" | "reject";
229
239
  to: string;
230
240
  assertBeforeRename?: () => void;
241
+ assertBeforeMutation?: () => void;
242
+ onDestinationPublished?: (receipt: MovePathPublicationReceipt) => void;
231
243
  };
232
244
  ```
233
245
 
234
- Throw to refuse publication. The original error is propagated, including errors
235
- with `EXDEV` or `EPERM` codes; an authorization failure never starts a copy
236
- fallback. A genuine rename failure may still require a second authorization
246
+ Throw to refuse the next mutation. The original error is propagated, including
247
+ errors with `EXDEV` or `EPERM` codes; an authorization failure never starts a
248
+ copy fallback. A genuine rename failure may still require a second authority
237
249
  check before publishing the staged copy.
238
250
 
239
- The callback must return `undefined` synchronously. Returning a Promise, thenable,
240
- or any other value refuses the rename with a `TypeError`; rejected asynchronous
241
- results are consumed without authorizing the operation. Perform asynchronous
242
- policy checks before calling the helper and use this callback to recheck the
243
- current owner at the mutation boundary.
251
+ All three callbacks must return `undefined` synchronously. Returning a Promise,
252
+ thenable, or any other value fails with a `TypeError`; rejected asynchronous
253
+ results are consumed. Perform asynchronous policy checks before calling the
254
+ helper and use the authority callback to recheck the current owner at each
255
+ mutation boundary. All callbacks are captured before the first await.
256
+
257
+ `onDestinationPublished` runs exactly once after a successful rename resolves,
258
+ before awaited post-rename directory checks or source cleanup. It receives a
259
+ frozen receipt with the resolved absolute destination path and the exact bigint
260
+ device/inode identity observed on the rename source before dispatch: the
261
+ original source for a direct rename, or the staged copy for fallback. Failed
262
+ rename attempts never emit receipts. The return contract remains `Promise<void>`.
263
+
264
+ The receipt records an observed identity, not authorization, durable storage,
265
+ or an atomic guarantee against concurrent pathname replacement. Before recovery,
266
+ recheck caller authority and compare the retained identity with a fresh bigint
267
+ stat of the destination. In particular, unknown Windows device/inode values
268
+ must not be treated as proof of ownership. Do not derive ownership from a new
269
+ post-failure snapshot alone.
244
270
 
245
271
  A refused rename leaves the source and destination unchanged; any private
246
- staged copy follows the helper's normal cleanup. The check does not cancel an
247
- already-dispatched rename or make an external lease store atomic with the
248
- filesystem. Cleanup after a successful move retains the existing source-identity
249
- checks. Omitting the
250
- callback preserves the usual move behavior.
272
+ staged copy follows normal cleanup. If publication has already happened, a
273
+ callback error or later verification failure preserves the published destination
274
+ and stops further source cleanup. Revocation during cleanup leaves all
275
+ as-yet-unremoved source entries intact; entries already removed remain removed.
276
+ The caller retains the receipt even if the move later rejects and owns recovery
277
+ from this partial-move state. An observer that throws must retain its receipt
278
+ before throwing if recovery needs it.
279
+
280
+ These callbacks do not cancel already-dispatched operations or make an external
281
+ lease store atomic with the filesystem. `assertBeforeMutation` guards renames
282
+ and removal of copied source entries; private staging creation, writes, and
283
+ failed-staging cleanup remain owned by the helper. Omitting the new callbacks
284
+ preserves the existing move and source-identity behavior.
251
285
 
252
286
  ## Difference from `root()`
253
287
 
package/docs/reading.md CHANGED
@@ -12,6 +12,8 @@ const opened = await fs.open("large.log"); // FileHandle for strea
12
12
 
13
13
  ## What every read does
14
14
 
15
+ Identity and containment checks use brief synchronous metadata calls, like Node's module resolution, while file data is read asynchronously. Canonical paths retain Node's native realpath spelling, including expansion of Windows short paths.
16
+
15
17
  Regardless of shape, every read goes through the same boundary checks:
16
18
 
17
19
  1. Resolve the input lexically against the canonical real root.
package/docs/root.md CHANGED
@@ -18,6 +18,7 @@ const fs = await root("/srv/workspace", {
18
18
  function root(rootDir: string, defaults?: RootDefaults): Promise<Root>;
19
19
 
20
20
  type RootDefaults = {
21
+ durable?: boolean; // fsync write/create/writeJson/createJson/append; default true
21
22
  hardlinks?: "reject" | "allow"; // refuse files with nlink > 1 on read; defaults to "reject"
22
23
  denyMutations?: DenyMutationPolicy; // absolute paths/prefixes mutation methods may not change
23
24
  maxBytes?: number; // refuse reads larger than this many bytes; defaults to 16 MiB
@@ -99,7 +100,7 @@ fs.write(rel, data, options?) // overwrite-ok atomic write
99
100
  fs.create(rel, data, options?) // throws "already-exists" if target exists
100
101
  fs.writeJson(rel, value, options?) // JSON.stringify + atomic write
101
102
  fs.createJson(rel, value, options?) // create() variant of writeJson
102
- fs.append(rel, data, options?) // append text/buffer; syncs before close
103
+ fs.append(rel, data, options?) // append text/buffer; syncs before close by default
103
104
  fs.copyIn(rel, sourceAbsPath, options?) // copy from outside the root, atomically, with size cap
104
105
  fs.openWritable(rel, options?) // FileHandle for streaming writes; supports await using
105
106
  fs.move(from, to, options?) // rename within the root; defaults to no clobber
@@ -110,6 +111,14 @@ fs.ensureRoot(options?) // accepts "" / "." as the root itself
110
111
 
111
112
  `write`, `create`, `append`, `writeJson`, and `createJson` accept `mode?: number`; use `0o600` for credentials and other private state. `writeJson` also accepts the same options as `JSON.stringify` plus `trailingNewline?: boolean` (defaults `true` so the file ends in `\n`).
112
113
 
114
+ These five methods also accept `durable?: boolean`: the per-call value overrides
115
+ `Root.defaults.durable`, which defaults to `true` when omitted. An explicitly
116
+ `undefined` per-call value preserves the root default. `durable: false` keeps
117
+ the existing publication behavior, modes, and identity checks but skips file
118
+ and parent-directory fsync calls. Use it only for reconstructible data: a crash
119
+ may lose the write or leave the previous file. See [Writing](writing.md#write-options)
120
+ for platform details.
121
+
113
122
  `copyIn` is a one-shot ingest from a trusted absolute source path: it streams the source through the boundary, atomically renames into the root, and respects `maxBytes`.
114
123
 
115
124
  Root operations that choose a new destination reject a leading Windows
@@ -66,8 +66,12 @@ Strings are UTF-8. `mode` is the requested **published** mode and defaults to
66
66
  `0600`; exact final modes, including `000`, are supported. The unpublished file
67
67
  stays at `0600` throughout preparation and any awaited application checks.
68
68
  After rename succeeds and the published entry passes identity validation, the
69
- owner applies the requested mode through its retained file descriptor and
70
- synchronizes the file. No parent is created or chmodded.
69
+ owner applies the requested mode through its retained file descriptor. Content
70
+ was synchronized during preparation; publication always synchronizes the parent.
71
+ Modes retaining owner read/write skip the extra mode-only file sync, so a crash
72
+ may leave the tighter staged `0600` instead of the wider requested mode. Modes
73
+ removing owner read or write, and corrections of an observed wider staged mode,
74
+ retain the post-chmod file sync. No parent is created or chmodded.
71
75
  Creation uses an exclusive, no-follow, close-on-exec open of a generated direct
72
76
  child name. Writes use that descriptor. Inspection uses non-following metadata
73
77
  operations, never a potentially blocking reopen of the leaf.
package/docs/types.md CHANGED
@@ -100,6 +100,7 @@ type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
100
100
 
101
101
  type RootDefaults = {
102
102
  denyMutations?: DenyMutationPolicy;
103
+ durable?: boolean; // default true for write/create/writeJson/createJson/append
103
104
  hardlinks?: "reject" | "allow";
104
105
  maxBytes?: number;
105
106
  mkdir?: boolean; // default true for mutation methods
@@ -126,7 +127,7 @@ type RootOptions = {
126
127
 
127
128
  ```ts
128
129
  type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "nonBlockingRead" | "symlinks">;
129
- type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "mkdir" | "mode" | "renameIdentity"> & {
130
+ type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "durable" | "mkdir" | "mode" | "renameIdentity"> & {
130
131
  encoding?: BufferEncoding;
131
132
  overwrite?: boolean;
132
133
  };
package/docs/writing.md CHANGED
@@ -77,13 +77,41 @@ await fs.write("state/last-run.json", JSON.stringify(run));
77
77
  await fs.write("notes/today.txt", "hello\n", { encoding: "utf8" });
78
78
  ```
79
79
 
80
- `data` accepts `string | Buffer`. `options` are `{ denyMutations?: DenyMutationPolicy; encoding?: BufferEncoding; mkdir?: boolean; mode?: number; overwrite?: boolean; renameIdentity?: RenameIdentityPolicy }`. `mode` sets the file's POSIX mode. If neither the call nor `RootDefaults` supplies it, a replacement preserves the existing file mode and a new file uses `0o600`. `mkdir` and `overwrite` both default to `true`; set `overwrite: false` for the same no-clobber behavior as `create()`.
80
+ `data` accepts `string | Buffer`. `mode` sets the file's POSIX mode. If neither the call nor `RootDefaults` supplies it, a replacement preserves the existing file mode and a new file uses `0o600`. `mkdir` and `overwrite` both default to `true`; set `overwrite: false` for the same no-clobber behavior as `create()`.
81
+
82
+ #### Write options
83
+
84
+ | Option | Type | Default / behavior |
85
+ |---|---|---|
86
+ | `denyMutations` | `DenyMutationPolicy` | Merged with root-level denies. |
87
+ | `durable` | `boolean` | `true`; use `false` to skip file and parent fsync. |
88
+ | `encoding` | `BufferEncoding` | `"utf8"` for strings. |
89
+ | `mkdir` | `boolean` | `true`; creates missing parents. |
90
+ | `mode` | `number` | Inherited on replacement, otherwise `0o600`. |
91
+ | `overwrite` | `boolean` | `true`; `false` is create-only. |
92
+ | `renameIdentity` | `RenameIdentityPolicy` | `"strict"`. |
93
+
94
+ `write`, `create`, `writeJson`, `createJson`, and `append` accept `durable`.
95
+ Precedence is per-call option, then `Root.defaults.durable`, then `true`;
96
+ an explicitly `undefined` call option preserves the root default.
97
+ `durable: false` keeps the sibling-temp replace/rename behavior of replacement
98
+ writes but skips file and parent-directory fsync calls. Create-only and append
99
+ publication behavior, permissions, identity checks, and error codes are unchanged.
100
+ Use it only for reconstructible data: a crash may lose the write or leave the
101
+ previous file. `copyIn`, `move`, and streaming `openWritable` do not use this option.
102
+ The existing pure-JavaScript Windows writer performs no fsync calls in either
103
+ setting; native Windows writes honor the option. Directory sync remains best-effort.
81
104
 
82
105
  POSIX modes without read permission, including `0o000` and `0o200`, succeed:
83
106
  final verification uses a descriptor retained by the writer rather than reopening
84
107
  the published file. The requested mode is not relaxed for verification.
85
108
  Publication verification compares exact bigint descriptor and pathname identities,
86
109
  including large file indexes that cannot be represented by a JavaScript number.
110
+ With durability enabled (the default), native publication syncs content before rename and syncs the parent directory.
111
+ Modes that retain owner read/write skip the extra mode-only file sync: after a
112
+ crash, the file may retain staged mode `0o600` instead of the wider requested mode.
113
+ Modes that remove owner read or write, and corrections of observed wider staging
114
+ permissions, keep the post-chmod file sync.
87
115
  Later reads still obey OS permissions, and access checks on a pre-existing
88
116
  destination are unchanged. The explicit FUSE compatibility policy still requires
89
117
  a readable destination to prove matching content when rename changes its identity.
@@ -138,7 +166,7 @@ type RootWriteJsonOptions = RootWriteOptions & {
138
166
 
139
167
  ### `fs.append(rel, data, options?)`
140
168
 
141
- Open in append mode, write, sync the file handle, and close. Honors `mkdir` for the parent directory and syncs the parent directory when the append creates the file. Pass `prependNewlineIfNeeded: true` to insert a `\n` if the file does not already end in one.
169
+ Open in append mode, write, sync the file handle, and close. Honors `mkdir` for the parent directory and syncs the parent directory when the append creates the file. `durable: false` skips both syncs. Pass `prependNewlineIfNeeded: true` to insert a `\n` if the file does not already end in one.
142
170
 
143
171
  ```ts
144
172
  await fs.append("logs/today.log", `[${ts}] ${line}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "keywords": [
6
6
  "filesystem",
@@ -156,13 +156,13 @@
156
156
  "archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
157
157
  },
158
158
  "optionalDependencies": {
159
- "@openclaw/fs-safe-darwin-arm64": "0.8.2",
160
- "@openclaw/fs-safe-darwin-x64": "0.8.2",
161
- "@openclaw/fs-safe-linux-arm64-gnu": "0.8.2",
162
- "@openclaw/fs-safe-linux-arm64-musl": "0.8.2",
163
- "@openclaw/fs-safe-linux-x64-gnu": "0.8.2",
164
- "@openclaw/fs-safe-linux-x64-musl": "0.8.2",
165
- "@openclaw/fs-safe-win32-x64-msvc": "0.8.2",
159
+ "@openclaw/fs-safe-darwin-arm64": "0.8.4",
160
+ "@openclaw/fs-safe-darwin-x64": "0.8.4",
161
+ "@openclaw/fs-safe-linux-arm64-gnu": "0.8.4",
162
+ "@openclaw/fs-safe-linux-arm64-musl": "0.8.4",
163
+ "@openclaw/fs-safe-linux-x64-gnu": "0.8.4",
164
+ "@openclaw/fs-safe-linux-x64-musl": "0.8.4",
165
+ "@openclaw/fs-safe-win32-x64-msvc": "0.8.4",
166
166
  "jszip": "^3.10.1"
167
167
  },
168
168
  "devDependencies": {
@@ -1,11 +0,0 @@
1
- import { type ExtractionDeadline } from "./archive-deadline.js";
2
- import type { ResolvedArchiveExtractLimits, TarMeterLimits } from "./archive-limits.js";
3
- import type { ExtractArchiveOptions } from "./archive-options.js";
4
- export declare function extractWasmTar(params: {
5
- archivePath: string;
6
- options: ExtractArchiveOptions;
7
- limits: ResolvedArchiveExtractLimits;
8
- tarLimits: TarMeterLimits;
9
- deadline: ExtractionDeadline;
10
- }): Promise<void>;
11
- //# sourceMappingURL=archive-tar-extract.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"archive-tar-extract.d.ts","sourceRoot":"","sources":["../src/archive-tar-extract.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAExF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAQlE,wBAAsB,cAAc,CAAC,MAAM,EAAE;IAC3C,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,qBAAqB,CAAC;IAAC,MAAM,EAAE,4BAA4B,CAAC;IAC1F,SAAS,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,kBAAkB,CAAC;CACzD,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChB"}
@@ -1,49 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { Readable } from "node:stream";
4
- import { stripArchivePath } from "./archive-entry.js";
5
- import {} from "./archive-deadline.js";
6
- import { mergePlannedArchiveIntoDestination } from "./archive-merge.js";
7
- import { resolveArchiveEntryMode } from "./archive-policy.js";
8
- import { prepareArchiveDestinationDir, preparePrivateArchiveOutputPath, withStagedArchiveDestination } from "./archive-staging.js";
9
- import { createTarEntryPreflightChecker } from "./archive-tar.js";
10
- import { inspectTar, replayTar } from "./archive-tar-stream.js";
11
- import { runPinnedWriteHelper } from "./pinned-write.js";
12
- export async function extractWasmTar(params) {
13
- const { options, deadline, tarLimits } = params;
14
- const manifest = [];
15
- await inspectTar({ archivePath: params.archivePath, limits: tarLimits, signal: deadline.signal,
16
- onMember: (entry) => { manifest.push(entry); } });
17
- deadline.check();
18
- const destinationRealDir = await prepareArchiveDestinationDir(options.destDir);
19
- await withStagedArchiveDestination({ destinationRealDir, run: async (stagingPath) => {
20
- const stagingDir = await fs.realpath(stagingPath);
21
- const check = createTarEntryPreflightChecker({ rootDir: destinationRealDir,
22
- stripComponents: options.stripComponents, limits: params.limits,
23
- entryFilter: options.entryFilter, onFiltered: options.onFiltered });
24
- const strip = Math.max(0, Math.floor(options.stripComponents ?? 0));
25
- const accepted = manifest.filter((entry) => { deadline.check(); return check(entry); }).map((entry) => {
26
- const kind = entry.type === "Directory" || entry.type === "GNUDumpDir" ? "directory" : "file";
27
- return { ...entry, path: stripArchivePath(entry.path, strip), kind,
28
- mode: resolveArchiveEntryMode({ kind, archivedMode: entry.mode, policy: options.entryModes }) };
29
- });
30
- await replayTar({ archivePath: params.archivePath, limits: tarLimits, signal: deadline.signal, members: accepted,
31
- async consume(member, payload) {
32
- deadline.check();
33
- await preparePrivateArchiveOutputPath({ destinationDir: stagingDir, destinationRealDir: stagingDir,
34
- relPath: member.path, outPath: path.join(stagingDir, member.path), originalPath: member.path,
35
- isDirectory: member.kind === "directory", deadline });
36
- if (member.kind === "file") {
37
- await runPinnedWriteHelper({ rootPath: stagingDir, relativeParentPath: path.posix.dirname(member.path),
38
- basename: path.posix.basename(member.path), mkdir: false, mode: 0o600, overwrite: false,
39
- maxBytes: member.size, input: { kind: "stream", stream: Readable.from(payload) } });
40
- }
41
- deadline.check();
42
- },
43
- });
44
- deadline.check();
45
- await mergePlannedArchiveIntoDestination({ entries: accepted, sourceDir: stagingDir,
46
- destinationDir: options.destDir, destinationRealDir, deadline });
47
- deadline.check();
48
- } });
49
- }
package/dist/fsync.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export { syncDirectoryBestEffort } from "./directory-durability.js";
2
- //# sourceMappingURL=fsync.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"fsync.d.ts","sourceRoot":"","sources":["../src/fsync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC"}
package/dist/fsync.js DELETED
@@ -1 +0,0 @@
1
- export { syncDirectoryBestEffort } from "./directory-durability.js";
@@ -1,9 +0,0 @@
1
- export declare function recoverDurableQueueRetirement(params: {
2
- jsonPath: string;
3
- processingPath: string;
4
- }): Promise<void>;
5
- export declare function retireDurableQueueSource(params: {
6
- jsonPath: string;
7
- processingPath: string;
8
- }): Promise<void>;
9
- //# sourceMappingURL=json-durable-queue-retirement.d.ts.map