@openclaw/fs-safe 0.4.7 → 0.5.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 (239) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +82 -43
  3. package/dist/archive-entry.d.ts.map +1 -1
  4. package/dist/archive-entry.js +5 -4
  5. package/dist/archive-errors.d.ts +11 -0
  6. package/dist/archive-errors.d.ts.map +1 -0
  7. package/dist/archive-errors.js +16 -0
  8. package/dist/archive-input.d.ts +12 -0
  9. package/dist/archive-input.d.ts.map +1 -0
  10. package/dist/archive-input.js +72 -0
  11. package/dist/archive-kind.d.ts +1 -1
  12. package/dist/archive-kind.d.ts.map +1 -1
  13. package/dist/archive-kind.js +18 -0
  14. package/dist/archive-limits.d.ts +10 -0
  15. package/dist/archive-limits.d.ts.map +1 -1
  16. package/dist/archive-limits.js +18 -0
  17. package/dist/archive-native.d.ts +18 -0
  18. package/dist/archive-native.d.ts.map +1 -0
  19. package/dist/archive-native.js +115 -0
  20. package/dist/archive-options.d.ts +21 -0
  21. package/dist/archive-options.d.ts.map +1 -0
  22. package/dist/archive-options.js +1 -0
  23. package/dist/archive-policy.d.ts +20 -0
  24. package/dist/archive-policy.d.ts.map +1 -0
  25. package/dist/archive-policy.js +29 -0
  26. package/dist/archive-read.d.ts +6 -0
  27. package/dist/archive-read.d.ts.map +1 -0
  28. package/dist/archive-read.js +200 -0
  29. package/dist/archive-staging.d.ts +2 -5
  30. package/dist/archive-staging.d.ts.map +1 -1
  31. package/dist/archive-staging.js +19 -10
  32. package/dist/archive-tar-meta.d.ts +6 -0
  33. package/dist/archive-tar-meta.d.ts.map +1 -0
  34. package/dist/archive-tar-meta.js +143 -0
  35. package/dist/archive-tar-runtime.d.ts +43 -0
  36. package/dist/archive-tar-runtime.d.ts.map +1 -0
  37. package/dist/archive-tar-runtime.js +8 -0
  38. package/dist/archive-tar.d.ts +5 -1
  39. package/dist/archive-tar.d.ts.map +1 -1
  40. package/dist/archive-tar.js +24 -6
  41. package/dist/archive-zip-entry.d.ts +15 -0
  42. package/dist/archive-zip-entry.d.ts.map +1 -0
  43. package/dist/archive-zip-entry.js +17 -0
  44. package/dist/archive.d.ts +7 -17
  45. package/dist/archive.d.ts.map +1 -1
  46. package/dist/archive.js +111 -108
  47. package/dist/atomic.d.ts +1 -1
  48. package/dist/atomic.d.ts.map +1 -1
  49. package/dist/config.d.ts +1 -1
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/config.js +1 -1
  52. package/dist/containment.d.ts +2 -0
  53. package/dist/containment.d.ts.map +1 -0
  54. package/dist/containment.js +1 -0
  55. package/dist/durability.d.ts +2 -0
  56. package/dist/durability.d.ts.map +1 -1
  57. package/dist/durability.js +2 -0
  58. package/dist/errors.d.ts +4 -1
  59. package/dist/errors.d.ts.map +1 -1
  60. package/dist/errors.js +2 -0
  61. package/dist/file-hash.d.ts +10 -0
  62. package/dist/file-hash.d.ts.map +1 -0
  63. package/dist/file-hash.js +73 -0
  64. package/dist/file-lock-sync.d.ts +35 -0
  65. package/dist/file-lock-sync.d.ts.map +1 -0
  66. package/dist/file-lock-sync.js +233 -0
  67. package/dist/file-lock.d.ts +2 -0
  68. package/dist/file-lock.d.ts.map +1 -1
  69. package/dist/file-lock.js +1 -0
  70. package/dist/filename.d.ts.map +1 -1
  71. package/dist/filename.js +4 -1
  72. package/dist/guarded-mkdir.d.ts +7 -1
  73. package/dist/guarded-mkdir.d.ts.map +1 -1
  74. package/dist/guarded-mkdir.js +45 -3
  75. package/dist/index.d.ts +4 -3
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +1 -1
  78. package/dist/json-document-store.d.ts.map +1 -1
  79. package/dist/json-document-store.js +30 -15
  80. package/dist/move-path.d.ts.map +1 -1
  81. package/dist/move-path.js +101 -17
  82. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  83. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  84. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  85. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  86. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  87. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  88. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
  89. package/dist/native-binding.d.ts +79 -0
  90. package/dist/native-binding.d.ts.map +1 -0
  91. package/dist/native-binding.js +1 -0
  92. package/dist/native-config.d.ts +17 -0
  93. package/dist/native-config.d.ts.map +1 -0
  94. package/dist/native-config.js +71 -0
  95. package/dist/native-operations.d.ts +21 -0
  96. package/dist/native-operations.d.ts.map +1 -0
  97. package/dist/native-operations.js +115 -0
  98. package/dist/native-pinned-write.d.ts +5 -0
  99. package/dist/native-pinned-write.d.ts.map +1 -0
  100. package/dist/native-pinned-write.js +115 -0
  101. package/dist/native.d.ts +14 -0
  102. package/dist/native.d.ts.map +1 -0
  103. package/dist/native.js +204 -0
  104. package/dist/output-sibling.d.ts +8 -0
  105. package/dist/output-sibling.d.ts.map +1 -0
  106. package/dist/output-sibling.js +122 -0
  107. package/dist/output.d.ts +2 -0
  108. package/dist/output.d.ts.map +1 -1
  109. package/dist/output.js +32 -5
  110. package/dist/owner-dacl.d.ts +31 -0
  111. package/dist/owner-dacl.d.ts.map +1 -0
  112. package/dist/owner-dacl.js +27 -0
  113. package/dist/path-policy.js +1 -1
  114. package/dist/permissions-public.d.ts +2 -0
  115. package/dist/permissions-public.d.ts.map +1 -1
  116. package/dist/permissions-public.js +2 -0
  117. package/dist/permissions.d.ts.map +1 -1
  118. package/dist/permissions.js +6 -0
  119. package/dist/pinned-operation.d.ts +2 -0
  120. package/dist/pinned-operation.d.ts.map +1 -0
  121. package/dist/pinned-operation.js +35 -0
  122. package/dist/pinned-write.d.ts +3 -16
  123. package/dist/pinned-write.d.ts.map +1 -1
  124. package/dist/pinned-write.js +20 -85
  125. package/dist/private-directory.d.ts +5 -0
  126. package/dist/private-directory.d.ts.map +1 -0
  127. package/dist/private-directory.js +13 -0
  128. package/dist/private-temp-workspace.d.ts +6 -2
  129. package/dist/private-temp-workspace.d.ts.map +1 -1
  130. package/dist/private-temp-workspace.js +40 -13
  131. package/dist/publish-file-failure.d.ts +27 -0
  132. package/dist/publish-file-failure.d.ts.map +1 -0
  133. package/dist/publish-file-failure.js +21 -0
  134. package/dist/publish-file.d.ts +21 -0
  135. package/dist/publish-file.d.ts.map +1 -0
  136. package/dist/publish-file.js +351 -0
  137. package/dist/read-opened-file.d.ts +3 -0
  138. package/dist/read-opened-file.d.ts.map +1 -1
  139. package/dist/read-opened-file.js +1 -0
  140. package/dist/replace-file-copy-fallback.d.ts +34 -0
  141. package/dist/replace-file-copy-fallback.d.ts.map +1 -0
  142. package/dist/replace-file-copy-fallback.js +339 -0
  143. package/dist/replace-file.d.ts +6 -2
  144. package/dist/replace-file.d.ts.map +1 -1
  145. package/dist/replace-file.js +39 -90
  146. package/dist/root-context.d.ts +4 -1
  147. package/dist/root-context.d.ts.map +1 -1
  148. package/dist/root-context.js +20 -1
  149. package/dist/root-impl.d.ts +5 -0
  150. package/dist/root-impl.d.ts.map +1 -1
  151. package/dist/root-impl.js +128 -225
  152. package/dist/root-path-existing.d.ts +3 -0
  153. package/dist/root-path-existing.d.ts.map +1 -0
  154. package/dist/root-path-existing.js +69 -0
  155. package/dist/root-path.d.ts +1 -2
  156. package/dist/root-path.d.ts.map +1 -1
  157. package/dist/root-path.js +67 -81
  158. package/dist/root-walk.d.ts +41 -0
  159. package/dist/root-walk.d.ts.map +1 -0
  160. package/dist/root-walk.js +114 -0
  161. package/dist/root.d.ts +2 -0
  162. package/dist/root.d.ts.map +1 -1
  163. package/dist/secret-file.d.ts +5 -2
  164. package/dist/secret-file.d.ts.map +1 -1
  165. package/dist/secret-file.js +20 -2
  166. package/dist/secret-read-async.d.ts +4 -0
  167. package/dist/secret-read-async.d.ts.map +1 -0
  168. package/dist/secret-read-async.js +95 -0
  169. package/dist/secret.d.ts +2 -1
  170. package/dist/secret.d.ts.map +1 -1
  171. package/dist/secret.js +2 -1
  172. package/dist/sibling-temp.d.ts.map +1 -1
  173. package/dist/sibling-temp.js +1 -0
  174. package/dist/sidecar-lock-handle.d.ts +20 -0
  175. package/dist/sidecar-lock-handle.d.ts.map +1 -0
  176. package/dist/sidecar-lock-handle.js +28 -0
  177. package/dist/sidecar-lock-policy.d.ts +10 -0
  178. package/dist/sidecar-lock-policy.d.ts.map +1 -0
  179. package/dist/sidecar-lock-policy.js +29 -0
  180. package/dist/sidecar-lock-reclaim.d.ts +22 -6
  181. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  182. package/dist/sidecar-lock-reclaim.js +106 -20
  183. package/dist/sidecar-lock-types.d.ts +56 -0
  184. package/dist/sidecar-lock-types.d.ts.map +1 -0
  185. package/dist/sidecar-lock-types.js +1 -0
  186. package/dist/sidecar-lock.d.ts +2 -45
  187. package/dist/sidecar-lock.d.ts.map +1 -1
  188. package/dist/sidecar-lock.js +94 -55
  189. package/dist/temp-cleanup.d.ts +7 -1
  190. package/dist/temp-cleanup.d.ts.map +1 -1
  191. package/dist/temp-cleanup.js +35 -5
  192. package/dist/temp.d.ts +2 -1
  193. package/dist/temp.d.ts.map +1 -1
  194. package/dist/test-hooks.d.ts +3 -0
  195. package/dist/test-hooks.d.ts.map +1 -1
  196. package/dist/windows-permissions-native.d.ts +9 -0
  197. package/dist/windows-permissions-native.d.ts.map +1 -0
  198. package/dist/windows-permissions-native.js +31 -0
  199. package/docs/archive.md +131 -13
  200. package/docs/atomic.md +30 -1
  201. package/docs/config.md +51 -21
  202. package/docs/durability.md +211 -1
  203. package/docs/errors.md +36 -6
  204. package/docs/filename.md +0 -0
  205. package/docs/index.md +11 -8
  206. package/docs/install.md +27 -26
  207. package/docs/json-store.md +18 -3
  208. package/docs/migrating-to-0.5.md +196 -0
  209. package/docs/native-helper.md +85 -0
  210. package/docs/native.md +134 -0
  211. package/docs/output.md +37 -11
  212. package/docs/path.md +1 -1
  213. package/docs/permissions.md +84 -1
  214. package/docs/quickstart.md +5 -2
  215. package/docs/reading.md +4 -4
  216. package/docs/root.md +31 -15
  217. package/docs/secret-file.md +50 -1
  218. package/docs/security-model.md +29 -8
  219. package/docs/sidecar-lock.md +92 -7
  220. package/docs/temp.md +24 -1
  221. package/docs/test-hooks.md +22 -2
  222. package/docs/testing.md +18 -7
  223. package/docs/types.md +3 -1
  224. package/docs/walk.md +55 -0
  225. package/docs/writing.md +3 -3
  226. package/package.json +8 -2
  227. package/dist/pinned-helper.d.ts +0 -9
  228. package/dist/pinned-helper.d.ts.map +0 -1
  229. package/dist/pinned-helper.js +0 -19
  230. package/dist/pinned-path.d.ts +0 -7
  231. package/dist/pinned-path.d.ts.map +0 -1
  232. package/dist/pinned-path.js +0 -21
  233. package/dist/pinned-python-config.d.ts +0 -9
  234. package/dist/pinned-python-config.d.ts.map +0 -1
  235. package/dist/pinned-python-config.js +0 -38
  236. package/dist/pinned-python.d.ts +0 -12
  237. package/dist/pinned-python.d.ts.map +0 -1
  238. package/dist/pinned-python.js +0 -688
  239. package/docs/python-helper.md +0 -103
@@ -0,0 +1,69 @@
1
+ import fs from "node:fs";
2
+ import fsp from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { isNotFoundPathError } from "./path.js";
5
+ function isFilesystemRoot(candidate) {
6
+ return path.parse(candidate).root === candidate;
7
+ }
8
+ async function pathExists(targetPath) {
9
+ try {
10
+ await fsp.lstat(targetPath);
11
+ return true;
12
+ }
13
+ catch (error) {
14
+ if (isNotFoundPathError(error)) {
15
+ return false;
16
+ }
17
+ throw error;
18
+ }
19
+ }
20
+ export async function resolvePathViaExistingAncestor(targetPath) {
21
+ const normalized = path.resolve(targetPath);
22
+ let cursor = normalized;
23
+ const missingSuffix = [];
24
+ while (!isFilesystemRoot(cursor) && !(await pathExists(cursor))) {
25
+ missingSuffix.unshift(path.basename(cursor));
26
+ const parent = path.dirname(cursor);
27
+ if (parent === cursor) {
28
+ break;
29
+ }
30
+ cursor = parent;
31
+ }
32
+ if (!(await pathExists(cursor))) {
33
+ return normalized;
34
+ }
35
+ try {
36
+ const resolvedAncestor = path.resolve(await fsp.realpath(cursor));
37
+ return missingSuffix.length === 0
38
+ ? resolvedAncestor
39
+ : path.resolve(resolvedAncestor, ...missingSuffix);
40
+ }
41
+ catch {
42
+ return normalized;
43
+ }
44
+ }
45
+ export function resolvePathViaExistingAncestorSync(targetPath) {
46
+ const normalized = path.resolve(targetPath);
47
+ let cursor = normalized;
48
+ const missingSuffix = [];
49
+ while (!isFilesystemRoot(cursor) && !fs.existsSync(cursor)) {
50
+ missingSuffix.unshift(path.basename(cursor));
51
+ const parent = path.dirname(cursor);
52
+ if (parent === cursor) {
53
+ break;
54
+ }
55
+ cursor = parent;
56
+ }
57
+ if (!fs.existsSync(cursor)) {
58
+ return normalized;
59
+ }
60
+ try {
61
+ const resolvedAncestor = path.resolve(fs.realpathSync(cursor));
62
+ return missingSuffix.length === 0
63
+ ? resolvedAncestor
64
+ : path.resolve(resolvedAncestor, ...missingSuffix);
65
+ }
66
+ catch {
67
+ return normalized;
68
+ }
69
+ }
@@ -1,3 +1,4 @@
1
+ export { resolvePathViaExistingAncestorSync } from "./root-path-existing.js";
1
2
  type RootPathIntent = "read" | "write" | "create" | "delete" | "stat";
2
3
  export type RootPathAliasPolicy = {
3
4
  allowFinalSymlinkForUnlink?: boolean;
@@ -34,6 +35,4 @@ export type ResolvedRootPath = {
34
35
  };
35
36
  export declare function resolveRootPath(params: ResolveRootPathParams): Promise<ResolvedRootPath>;
36
37
  export declare function resolveRootPathSync(params: ResolveRootPathParams): ResolvedRootPath;
37
- export declare function resolvePathViaExistingAncestorSync(targetPath: string): string;
38
- export {};
39
38
  //# sourceMappingURL=root-path.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"root-path.d.ts","sourceRoot":"","sources":["../src/root-path.ts"],"names":[],"mappings":"AAMA,KAAK,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEtE,MAAM,MAAM,mBAAmB,GAAG;IAChC,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC,CAAC;AAEF,eAAO,MAAM,wBAAwB;aACnC,MAAM;;;;aAIN,YAAY;;;;CAIJ,CAAC;AAEX,KAAK,qBAAqB,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,KAAK,oBAAoB,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,oBAAoB,CAAC;CAC5B,CAAC;AAEF,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,gBAAgB,CAAC,CA+B3B;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,qBAAqB,GAAG,gBAAgB,CA+BnF;AAmkBD,wBAAgB,kCAAkC,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA6B7E"}
1
+ {"version":3,"file":"root-path.d.ts","sourceRoot":"","sources":["../src/root-path.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,kCAAkC,EAAE,MAAM,yBAAyB,CAAC;AAE7E,KAAK,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEtE,MAAM,MAAM,mBAAmB,GAAG;IAChC,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC,CAAC;AAEF,eAAO,MAAM,wBAAwB;aACnC,MAAM;;;;aAIN,YAAY;;;;CAIJ,CAAC;AAEX,KAAK,qBAAqB,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,KAAK,oBAAoB,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,oBAAoB,CAAC;CAC5B,CAAC;AAEF,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,gBAAgB,CAAC,CA+B3B;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,qBAAqB,GAAG,gBAAgB,CA+BnF"}
package/dist/root-path.js CHANGED
@@ -3,6 +3,8 @@ import fsp from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { isNotFoundPathError, isPathInside, isPathRelativeEscape } from "./path.js";
6
+ import { resolvePathViaExistingAncestor, resolvePathViaExistingAncestorSync, } from "./root-path-existing.js";
7
+ export { resolvePathViaExistingAncestorSync } from "./root-path-existing.js";
6
8
  export const ROOT_PATH_ALIAS_POLICIES = {
7
9
  strict: Object.freeze({
8
10
  allowFinalSymlinkForUnlink: false,
@@ -80,15 +82,40 @@ function isPromiseLike(value) {
80
82
  typeof value.then === "function");
81
83
  }
82
84
  function createLexicalTraversalState(params) {
83
- const relative = path.relative(params.rootPath, params.absolutePath);
85
+ const rawAbsolutePath = params.params.absolutePath;
86
+ const rawRelativePath = rawPathRelativeToRoot(params.rootPath, rawAbsolutePath);
87
+ const relative = rawRelativePath ?? path.relative(params.rootPath, params.absolutePath);
84
88
  return {
85
- segments: relative.split(path.sep).filter(Boolean),
89
+ segments: splitTraversalSegments(relative),
86
90
  allowFinalSymlink: params.params.policy?.allowFinalSymlinkForUnlink === true,
87
91
  canonicalCursor: params.rootCanonicalPath,
88
92
  lexicalCursor: params.rootPath,
89
93
  preserveFinalSymlink: false,
90
94
  };
91
95
  }
96
+ function splitTraversalSegments(value) {
97
+ return value
98
+ .split(process.platform === "win32" ? /[\\/]+/ : /\/+/)
99
+ .filter((segment) => Boolean(segment) && segment !== ".");
100
+ }
101
+ function rawPathRelativeToRoot(rootPath, candidatePath) {
102
+ if (!path.isAbsolute(candidatePath)) {
103
+ return undefined;
104
+ }
105
+ const root = path.resolve(rootPath);
106
+ const candidate = process.platform === "win32"
107
+ ? candidatePath.replaceAll("/", path.sep)
108
+ : candidatePath;
109
+ if (candidate === root) {
110
+ return "";
111
+ }
112
+ const rootWithSep = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
113
+ const candidatePrefix = candidate.slice(0, rootWithSep.length);
114
+ const prefixMatches = process.platform === "win32"
115
+ ? candidatePrefix.toLowerCase() === rootWithSep.toLowerCase()
116
+ : candidatePrefix === rootWithSep;
117
+ return prefixMatches ? candidate.slice(rootWithSep.length) : undefined;
118
+ }
92
119
  function assertLexicalCursorInsideBoundary(params) {
93
120
  assertInsideBoundary({
94
121
  boundaryLabel: params.params.boundaryLabel,
@@ -99,13 +126,15 @@ function assertLexicalCursorInsideBoundary(params) {
99
126
  }
100
127
  function applyMissingSuffixToCanonicalCursor(params) {
101
128
  const missingSuffix = params.state.segments.slice(params.missingFromIndex);
102
- params.state.canonicalCursor = path.resolve(params.state.canonicalCursor, ...missingSuffix);
103
- assertLexicalCursorInsideBoundary({
104
- params: params.params,
105
- rootCanonicalPath: params.rootCanonicalPath,
106
- candidatePath: params.state.canonicalCursor,
107
- absolutePath: params.absolutePath,
108
- });
129
+ for (const segment of missingSuffix) {
130
+ advanceCanonicalCursorForSegment({
131
+ state: params.state,
132
+ segment,
133
+ rootCanonicalPath: params.rootCanonicalPath,
134
+ params: params.params,
135
+ absolutePath: params.absolutePath,
136
+ });
137
+ }
109
138
  }
110
139
  function advanceCanonicalCursorForSegment(params) {
111
140
  params.state.canonicalCursor = path.resolve(params.state.canonicalCursor, params.segment);
@@ -221,11 +250,20 @@ function resolveAndApplySymlinkHop(params) {
221
250
  boundaryLabel: params.boundaryLabel,
222
251
  });
223
252
  }
253
+ function applyParentTraversalStep(params) {
254
+ params.state.lexicalCursor = path.resolve(params.state.lexicalCursor, "..");
255
+ advanceCanonicalCursorForSegment({
256
+ state: params.state,
257
+ segment: "..",
258
+ rootCanonicalPath: params.rootCanonicalPath,
259
+ params: params.resolveParams,
260
+ absolutePath: params.absolutePath,
261
+ });
262
+ }
224
263
  function* iterateLexicalTraversal(state) {
225
264
  for (let idx = 0; idx < state.segments.length; idx += 1) {
226
265
  const segment = state.segments[idx] ?? "";
227
266
  const isLast = idx === state.segments.length - 1;
228
- state.lexicalCursor = path.join(state.lexicalCursor, segment);
229
267
  yield { idx, segment, isLast };
230
268
  }
231
269
  }
@@ -238,6 +276,14 @@ async function resolveRootPathLexicalAsync(params) {
238
276
  absolutePath: params.absolutePath,
239
277
  };
240
278
  for (const { idx, segment, isLast } of iterateLexicalTraversal(state)) {
279
+ if (segment === "..") {
280
+ applyParentTraversalStep({
281
+ ...sharedStepParams,
282
+ resolveParams: params.params,
283
+ });
284
+ continue;
285
+ }
286
+ state.lexicalCursor = path.join(state.lexicalCursor, segment);
241
287
  const stat = await readLexicalStat({
242
288
  ...sharedStepParams,
243
289
  missingFromIndex: idx,
@@ -265,7 +311,7 @@ async function resolveRootPathLexicalAsync(params) {
265
311
  resolveLinkCanonical: (cursor) => resolveSymlinkHopPath(cursor),
266
312
  });
267
313
  }
268
- const kind = await getPathKind(params.absolutePath, state.preserveFinalSymlink);
314
+ const kind = await getPathKind(state.canonicalCursor, state.preserveFinalSymlink);
269
315
  return finalizeLexicalResolution({
270
316
  ...params,
271
317
  state,
@@ -277,6 +323,15 @@ function resolveRootPathLexicalSync(params) {
277
323
  for (let idx = 0; idx < state.segments.length; idx += 1) {
278
324
  const segment = state.segments[idx] ?? "";
279
325
  const isLast = idx === state.segments.length - 1;
326
+ if (segment === "..") {
327
+ applyParentTraversalStep({
328
+ state,
329
+ rootCanonicalPath: params.rootCanonicalPath,
330
+ resolveParams: params.params,
331
+ absolutePath: params.absolutePath,
332
+ });
333
+ continue;
334
+ }
280
335
  state.lexicalCursor = path.join(state.lexicalCursor, segment);
281
336
  const maybeStat = readLexicalStat({
282
337
  state,
@@ -318,7 +373,7 @@ function resolveRootPathLexicalSync(params) {
318
373
  throw new Error("Unexpected async symlink resolution");
319
374
  }
320
375
  }
321
- const kind = getPathKindSync(params.absolutePath, state.preserveFinalSymlink);
376
+ const kind = getPathKindSync(state.canonicalCursor, state.preserveFinalSymlink);
322
377
  return finalizeLexicalResolution({
323
378
  ...params,
324
379
  state,
@@ -434,60 +489,6 @@ function buildResolvedRootPath(params) {
434
489
  kind: params.kind.kind,
435
490
  };
436
491
  }
437
- async function resolvePathViaExistingAncestor(targetPath) {
438
- const normalized = path.resolve(targetPath);
439
- let cursor = normalized;
440
- const missingSuffix = [];
441
- while (!isFilesystemRoot(cursor) && !(await pathExists(cursor))) {
442
- missingSuffix.unshift(path.basename(cursor));
443
- const parent = path.dirname(cursor);
444
- if (parent === cursor) {
445
- break;
446
- }
447
- cursor = parent;
448
- }
449
- if (!(await pathExists(cursor))) {
450
- return normalized;
451
- }
452
- try {
453
- const resolvedAncestor = path.resolve(await fsp.realpath(cursor));
454
- if (missingSuffix.length === 0) {
455
- return resolvedAncestor;
456
- }
457
- return path.resolve(resolvedAncestor, ...missingSuffix);
458
- }
459
- catch {
460
- return normalized;
461
- }
462
- }
463
- export function resolvePathViaExistingAncestorSync(targetPath) {
464
- const normalized = path.resolve(targetPath);
465
- let cursor = normalized;
466
- const missingSuffix = [];
467
- while (!isFilesystemRoot(cursor) && !fs.existsSync(cursor)) {
468
- missingSuffix.unshift(path.basename(cursor));
469
- const parent = path.dirname(cursor);
470
- if (parent === cursor) {
471
- break;
472
- }
473
- cursor = parent;
474
- }
475
- if (!fs.existsSync(cursor)) {
476
- return normalized;
477
- }
478
- try {
479
- // Keep sync behavior aligned with async (`fsp.realpath`) to avoid
480
- // platform-specific canonical alias drift (notably on Windows).
481
- const resolvedAncestor = path.resolve(fs.realpathSync(cursor));
482
- if (missingSuffix.length === 0) {
483
- return resolvedAncestor;
484
- }
485
- return path.resolve(resolvedAncestor, ...missingSuffix);
486
- }
487
- catch {
488
- return normalized;
489
- }
490
- }
491
492
  async function getPathKind(absolutePath, preserveFinalSymlink) {
492
493
  try {
493
494
  const stat = preserveFinalSymlink
@@ -555,21 +556,6 @@ function shortPath(value) {
555
556
  }
556
557
  return value;
557
558
  }
558
- function isFilesystemRoot(candidate) {
559
- return path.parse(candidate).root === candidate;
560
- }
561
- async function pathExists(targetPath) {
562
- try {
563
- await fsp.lstat(targetPath);
564
- return true;
565
- }
566
- catch (error) {
567
- if (isNotFoundPathError(error)) {
568
- return false;
569
- }
570
- throw error;
571
- }
572
- }
573
559
  async function resolveSymlinkHopPath(symlinkPath) {
574
560
  try {
575
561
  return path.resolve(await fsp.realpath(symlinkPath));
@@ -0,0 +1,41 @@
1
+ import type { DirEntry } from "./types.js";
2
+ export type RootWalkSymlinkPolicy = "skip" | "follow-within-root";
3
+ export type RootWalkLimitBehavior = "truncate" | "throw";
4
+ export type RootWalkDirectoryErrorBehavior = "throw" | "skip-and-report";
5
+ export type RootWalkEntryFilterResult = "include" | "skip" | "skip-subtree";
6
+ export type RootWalkDataEntryKind = "file" | "directory" | "other";
7
+ export type RootWalkEntryKind = RootWalkDataEntryKind | "directory-error" | "truncated";
8
+ export type RootWalkDataEntry = {
9
+ relativePath: string;
10
+ kind: RootWalkDataEntryKind;
11
+ size: number;
12
+ };
13
+ export type RootWalkEntry = RootWalkDataEntry | {
14
+ relativePath: string;
15
+ kind: "truncated";
16
+ size: 0;
17
+ } | {
18
+ relativePath: string;
19
+ kind: "directory-error";
20
+ size: 0;
21
+ error: unknown;
22
+ };
23
+ export type RootWalkEntryFilter = (entry: RootWalkDataEntry) => RootWalkEntryFilterResult;
24
+ export type RootWalkOptions = {
25
+ maxDepth?: number;
26
+ maxEntries?: number;
27
+ symlinkPolicy: RootWalkSymlinkPolicy;
28
+ signal?: AbortSignal;
29
+ limitBehavior?: RootWalkLimitBehavior;
30
+ entryFilter?: RootWalkEntryFilter;
31
+ onDirectoryError?: RootWalkDirectoryErrorBehavior;
32
+ };
33
+ type RootWalkCapability = {
34
+ rootReal: string;
35
+ list(relativePath: string, options: {
36
+ withFileTypes: true;
37
+ }): Promise<DirEntry[]>;
38
+ };
39
+ export declare function walkRoot(root: RootWalkCapability, relativePath: string, options: RootWalkOptions): AsyncGenerator<RootWalkEntry>;
40
+ export {};
41
+ //# sourceMappingURL=root-walk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-walk.d.ts","sourceRoot":"","sources":["../src/root-walk.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,oBAAoB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,OAAO,CAAC;AACzD,MAAM,MAAM,8BAA8B,GAAG,OAAO,GAAG,iBAAiB,CAAC;AACzE,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,MAAM,GAAG,cAAc,CAAC;AAC5E,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC;AACnE,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,WAAW,CAAC;AAExF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB,iBAAiB,GACjB;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACpD;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/E,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,yBAAyB,CAAC;AAE1F,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,qBAAqB,CAAC;IACrC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,aAAa,CAAC,EAAE,qBAAqB,CAAC;IACtC,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,gBAAgB,CAAC,EAAE,8BAA8B,CAAC;CACnD,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,aAAa,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;CACnF,CAAC;AAqBF,wBAAuB,QAAQ,CAC7B,IAAI,EAAE,kBAAkB,EACxB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,eAAe,GACvB,cAAc,CAAC,aAAa,CAAC,CAgG/B"}
@@ -0,0 +1,114 @@
1
+ import path from "node:path";
2
+ import { FsSafeError } from "./errors.js";
3
+ import { resolveRootPath } from "./root-path.js";
4
+ function validateBudget(name, value) {
5
+ if (value === undefined)
6
+ return Number.POSITIVE_INFINITY;
7
+ if (!Number.isSafeInteger(value) || value < 0) {
8
+ throw new RangeError(`${name} must be a non-negative safe integer`);
9
+ }
10
+ return value;
11
+ }
12
+ function entryKind(entry) {
13
+ if (entry.isSymbolicLink)
14
+ return "symlink";
15
+ if (entry.isDirectory)
16
+ return "directory";
17
+ if (entry.isFile)
18
+ return "file";
19
+ return "other";
20
+ }
21
+ function limitEntry(relativePath) {
22
+ return { relativePath, kind: "truncated", size: 0 };
23
+ }
24
+ export async function* walkRoot(root, relativePath, options) {
25
+ const maxDepth = validateBudget("maxDepth", options.maxDepth);
26
+ const maxEntries = validateBudget("maxEntries", options.maxEntries);
27
+ const visitedDirectories = new Set();
28
+ let examined = 0;
29
+ const onLimit = (atPath) => {
30
+ if ((options.limitBehavior ?? "truncate") === "throw") {
31
+ throw new FsSafeError("too-large", `root walk budget exceeded at ${atPath || "."}`);
32
+ }
33
+ return limitEntry(atPath);
34
+ };
35
+ async function* visit(directory, depth) {
36
+ options.signal?.throwIfAborted();
37
+ let entries;
38
+ try {
39
+ const resolvedDirectory = await resolveRootPath({
40
+ absolutePath: path.resolve(root.rootReal, directory),
41
+ rootPath: root.rootReal,
42
+ rootCanonicalPath: root.rootReal,
43
+ boundaryLabel: "root walk",
44
+ });
45
+ if (!resolvedDirectory.exists || resolvedDirectory.kind !== "directory") {
46
+ throw new FsSafeError("not-file", `root walk path is not a directory: ${directory || "."}`);
47
+ }
48
+ if (visitedDirectories.has(resolvedDirectory.canonicalPath)) {
49
+ return;
50
+ }
51
+ visitedDirectories.add(resolvedDirectory.canonicalPath);
52
+ const listingDirectory = path
53
+ .relative(root.rootReal, resolvedDirectory.canonicalPath)
54
+ .split(path.sep)
55
+ .join(path.posix.sep);
56
+ entries = await root.list(listingDirectory, { withFileTypes: true });
57
+ }
58
+ catch (error) {
59
+ if ((options.onDirectoryError ?? "throw") === "throw")
60
+ throw error;
61
+ yield { relativePath: directory, kind: "directory-error", size: 0, error };
62
+ return;
63
+ }
64
+ for (const entry of entries) {
65
+ options.signal?.throwIfAborted();
66
+ const child = directory
67
+ ? path.posix.join(directory.split(path.sep).join(path.posix.sep), entry.name)
68
+ : entry.name;
69
+ if (examined >= maxEntries) {
70
+ yield onLimit(child);
71
+ return;
72
+ }
73
+ examined += 1;
74
+ let kind = entryKind(entry);
75
+ let size = entry.size;
76
+ if (kind === "symlink") {
77
+ if (options.symlinkPolicy === "skip") {
78
+ continue;
79
+ }
80
+ const resolved = await resolveRootPath({
81
+ absolutePath: path.resolve(root.rootReal, child),
82
+ rootPath: root.rootReal,
83
+ rootCanonicalPath: root.rootReal,
84
+ boundaryLabel: "root walk",
85
+ });
86
+ if (!resolved.exists) {
87
+ continue;
88
+ }
89
+ kind = resolved.kind === "directory" ? "directory" : resolved.kind === "file" ? "file" : "other";
90
+ size = entry.size;
91
+ }
92
+ const walkEntry = { relativePath: child, kind, size };
93
+ const filterResult = options.entryFilter?.(walkEntry) ?? "include";
94
+ if (!["include", "skip", "skip-subtree"].includes(filterResult)) {
95
+ throw new TypeError(`invalid root walk entryFilter result: ${String(filterResult)}`);
96
+ }
97
+ if (filterResult === "include") {
98
+ yield walkEntry;
99
+ }
100
+ if (kind !== "directory") {
101
+ continue;
102
+ }
103
+ if (filterResult === "skip-subtree") {
104
+ continue;
105
+ }
106
+ if (depth >= maxDepth) {
107
+ yield onLimit(child);
108
+ return;
109
+ }
110
+ yield* visit(child, depth + 1);
111
+ }
112
+ }
113
+ yield* visit(relativePath, 0);
114
+ }
package/dist/root.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export { DEFAULT_ROOT_MAX_BYTES, openLocalFileSafely, readLocalFileSafely, resolveOpenedFileRealPathForHandle, root, type DenyMutationPolicy, type HardlinkPolicy, type RenameIdentityPolicy, type OpenResult, type ReadResult, type Root, type RootAppendOptions, type RootCopyOptions, type RootCreateJsonOptions, type RootCreateOptions, type RootDefaults, type RootMkdirOptions, type RootMoveOptions, type RootOpenOptions, type RootOpenWritableOptions, type RootOptions, type RootReadOptions, type RootRemoveOptions, type RootWriteJsonOptions, type RootWriteOptions, type SymlinkPolicy, type WritableOpenMode, type WritableOpenResult, } from "./root-impl.js";
2
+ export type { ContainmentGuarantee } from "./containment.js";
3
+ export type { RootWalkDataEntry, RootWalkDataEntryKind, RootWalkDirectoryErrorBehavior, RootWalkEntry, RootWalkEntryFilter, RootWalkEntryFilterResult, RootWalkEntryKind, RootWalkLimitBehavior, RootWalkOptions, RootWalkSymlinkPolicy, } from "./root-walk.js";
2
4
  //# sourceMappingURL=root.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../src/root.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,kCAAkC,EAClC,IAAI,EACJ,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../src/root.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,kCAAkC,EAClC,IAAI,EACJ,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,8BAA8B,EAC9B,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,qBAAqB,GACtB,MAAM,gBAAgB,CAAC"}
@@ -8,11 +8,14 @@ export type SecretFileReadOptions = {
8
8
  };
9
9
  export declare function readSecretFileSync(filePath: string, label: string, options?: SecretFileReadOptions): string;
10
10
  export declare function tryReadSecretFileSync(filePath: string | undefined, label: string, options?: SecretFileReadOptions): string | undefined;
11
- export declare function writeSecretFileAtomic(params: {
11
+ type SecretFileWriteParams = {
12
12
  rootDir: string;
13
13
  filePath: string;
14
14
  content: string | Uint8Array;
15
15
  mode?: number;
16
16
  dirMode?: number;
17
- }): Promise<void>;
17
+ };
18
+ export declare function writeSecretFileAtomic(params: SecretFileWriteParams): Promise<void>;
19
+ export declare function createSecretFileAtomic(params: SecretFileWriteParams): Promise<void>;
20
+ export {};
18
21
  //# sourceMappingURL=secret-file.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,6BAA6B,QAAY,CAAC;AACvD,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAqIF,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAQR;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAcpB;AAoID,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+ChB"}
1
+ {"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,6BAA6B,QAAY,CAAC;AACvD,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAqIF,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAQR;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAcpB;AAoID,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAyDF,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAExF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYzF"}
@@ -257,7 +257,7 @@ async function ensurePrivateDirectory(rootDir, targetDir, mode) {
257
257
  }
258
258
  return { rootGuard, targetReal: await fsp.realpath(resolvedTarget) };
259
259
  }
260
- export async function writeSecretFileAtomic(params) {
260
+ async function materializeSecretFileAtomic(params, createOnly) {
261
261
  const mode = params.mode ?? PRIVATE_SECRET_FILE_MODE;
262
262
  const dirMode = params.dirMode ?? PRIVATE_SECRET_DIR_MODE;
263
263
  const resolvedRoot = path.resolve(params.rootDir);
@@ -272,6 +272,9 @@ export async function writeSecretFileAtomic(params) {
272
272
  const finalFilePath = path.join(targetReal, fileName);
273
273
  try {
274
274
  const stat = await fsp.lstat(finalFilePath);
275
+ if (createOnly) {
276
+ throw new FsSafeError("secret-exists", `Private secret file ${finalFilePath} already exists.`);
277
+ }
275
278
  if (stat.isSymbolicLink()) {
276
279
  throw new Error(`Private secret file ${finalFilePath} must not be a symlink.`);
277
280
  }
@@ -292,7 +295,7 @@ export async function writeSecretFileAtomic(params) {
292
295
  basename: fileName,
293
296
  mkdir: false,
294
297
  mode,
295
- overwrite: true,
298
+ overwrite: !createOnly,
296
299
  input: { kind: "buffer", data: typeof params.content === "string" ? params.content : Buffer.from(params.content) },
297
300
  rootIdentity: { dev: parentGuard.stat.dev, ino: parentGuard.stat.ino },
298
301
  });
@@ -300,3 +303,18 @@ export async function writeSecretFileAtomic(params) {
300
303
  await assertAsyncDirectoryGuard(parentGuard);
301
304
  await enforcePrivateFileIdentityAndMode(finalFilePath, identity, mode);
302
305
  }
306
+ export async function writeSecretFileAtomic(params) {
307
+ await materializeSecretFileAtomic(params, false);
308
+ }
309
+ export async function createSecretFileAtomic(params) {
310
+ try {
311
+ await materializeSecretFileAtomic(params, true);
312
+ }
313
+ catch (error) {
314
+ if ((error instanceof FsSafeError && error.code === "already-exists") ||
315
+ error.code === "EEXIST") {
316
+ throw new FsSafeError("secret-exists", "Private secret file already exists.", { cause: error });
317
+ }
318
+ throw error;
319
+ }
320
+ }
@@ -0,0 +1,4 @@
1
+ import { type SecretFileReadOptions } from "./secret-file.js";
2
+ export declare function readSecretFile(filePath: string, label: string, options?: SecretFileReadOptions): Promise<string>;
3
+ export declare function tryReadSecretFile(filePath: string | undefined, label: string, options?: SecretFileReadOptions): Promise<string | undefined>;
4
+ //# sourceMappingURL=secret-read-async.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,kBAAkB,CAAC;AAqF1B,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAM7B"}
@@ -0,0 +1,95 @@
1
+ import fsSync from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import { readFileHandleBounded } from "./bounded-read.js";
4
+ import { FsSafeError } from "./errors.js";
5
+ import { sameFileIdentity } from "./file-identity.js";
6
+ import { resolveHomeRelativePath } from "./home-dir.js";
7
+ import { DEFAULT_SECRET_FILE_MAX_BYTES, } from "./secret-file.js";
8
+ function pathErrorCode(error) {
9
+ const code = error.code;
10
+ return code === "ENOENT" || code === "ENOTDIR" ? "not-found" : "invalid-path";
11
+ }
12
+ async function readSecretFileOutcome(filePath, label, options) {
13
+ const resolvedPath = resolveHomeRelativePath(filePath.trim());
14
+ if (!resolvedPath) {
15
+ return { ok: false, code: "invalid-path", message: `${label} file path is empty.` };
16
+ }
17
+ const maxBytes = options.maxBytes ?? DEFAULT_SECRET_FILE_MAX_BYTES;
18
+ let previewStat;
19
+ try {
20
+ previewStat = await fs.lstat(resolvedPath);
21
+ if (previewStat.isSymbolicLink()) {
22
+ if (options.rejectSymlink) {
23
+ return { ok: false, code: "symlink", message: `${label} file at ${resolvedPath} must not be a symlink.` };
24
+ }
25
+ previewStat = await fs.stat(resolvedPath);
26
+ }
27
+ }
28
+ catch (error) {
29
+ const normalized = error instanceof Error ? error : new Error(String(error));
30
+ return {
31
+ ok: false,
32
+ code: pathErrorCode(error),
33
+ error: normalized,
34
+ message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(normalized)}`,
35
+ };
36
+ }
37
+ if (!previewStat.isFile()) {
38
+ return { ok: false, code: "not-file", message: `${label} file at ${resolvedPath} must be a regular file.` };
39
+ }
40
+ if (options.rejectHardlinks !== false && previewStat.nlink > 1) {
41
+ return { ok: false, code: "hardlink", message: `${label} file at ${resolvedPath} must not be hardlinked.` };
42
+ }
43
+ if (previewStat.size > maxBytes) {
44
+ return { ok: false, code: "too-large", message: `${label} file at ${resolvedPath} exceeds ${maxBytes} bytes.` };
45
+ }
46
+ let handle;
47
+ try {
48
+ const realPath = await fs.realpath(resolvedPath);
49
+ const noFollow = process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
50
+ ? fsSync.constants.O_NOFOLLOW
51
+ : 0;
52
+ handle = await fs.open(realPath, fsSync.constants.O_RDONLY | noFollow);
53
+ const openedStat = await handle.stat();
54
+ const pathStat = await fs.lstat(realPath);
55
+ if (!openedStat.isFile() ||
56
+ !pathStat.isFile() ||
57
+ !sameFileIdentity(previewStat, openedStat) ||
58
+ !sameFileIdentity(pathStat, openedStat) ||
59
+ (options.rejectHardlinks !== false && openedStat.nlink > 1)) {
60
+ throw new FsSafeError("path-mismatch", "security validation failed");
61
+ }
62
+ const secret = (await readFileHandleBounded(handle, maxBytes)).toString("utf8").trim();
63
+ return secret
64
+ ? { ok: true, secret }
65
+ : { ok: false, code: "invalid-path", message: `${label} file at ${resolvedPath} is empty.` };
66
+ }
67
+ catch (error) {
68
+ const normalized = error instanceof Error ? error : new Error(String(error));
69
+ return {
70
+ ok: false,
71
+ code: error instanceof FsSafeError ? error.code : pathErrorCode(error) === "not-found" ? "not-found" : "path-mismatch",
72
+ error: normalized,
73
+ message: `Failed to read ${label} file at ${resolvedPath}: ${String(normalized)}`,
74
+ };
75
+ }
76
+ finally {
77
+ await handle?.close().catch(() => undefined);
78
+ }
79
+ }
80
+ export async function readSecretFile(filePath, label, options = {}) {
81
+ const result = await readSecretFileOutcome(filePath, label, options);
82
+ if (result.ok)
83
+ return result.secret;
84
+ throw new FsSafeError(result.code, result.message, { cause: result.error });
85
+ }
86
+ export async function tryReadSecretFile(filePath, label, options = {}) {
87
+ if (!filePath?.trim())
88
+ return undefined;
89
+ const result = await readSecretFileOutcome(filePath, label, options);
90
+ if (result.ok)
91
+ return result.secret;
92
+ if (result.code === "not-found")
93
+ return undefined;
94
+ throw new FsSafeError(result.code, result.message, { cause: result.error });
95
+ }