@openclaw/fs-safe 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +12 -7
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +32 -11
  5. package/dist/advanced.d.ts +2 -0
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +2 -0
  8. package/dist/archive-input.d.ts.map +1 -1
  9. package/dist/archive-input.js +6 -2
  10. package/dist/archive-merge.d.ts.map +1 -1
  11. package/dist/archive-merge.js +15 -2
  12. package/dist/archive-native.d.ts.map +1 -1
  13. package/dist/archive-native.js +7 -19
  14. package/dist/archive-plan.js +1 -1
  15. package/dist/archive-read.d.ts.map +1 -1
  16. package/dist/archive-read.js +18 -13
  17. package/dist/archive-staging.d.ts.map +1 -1
  18. package/dist/archive-staging.js +43 -10
  19. package/dist/archive-tar-inspect.d.ts.map +1 -1
  20. package/dist/archive-tar-inspect.js +4 -1
  21. package/dist/archive-zip-admission.d.ts +1 -1
  22. package/dist/archive-zip-admission.d.ts.map +1 -1
  23. package/dist/archive-zip-admission.js +2 -2
  24. package/dist/archive-zip-directory.d.ts +3 -0
  25. package/dist/archive-zip-directory.d.ts.map +1 -1
  26. package/dist/archive-zip-directory.js +13 -2
  27. package/dist/archive-zip-loader.d.ts +3 -2
  28. package/dist/archive-zip-loader.d.ts.map +1 -1
  29. package/dist/archive-zip-loader.js +30 -4
  30. package/dist/archive-zip-manifest.d.ts +5 -0
  31. package/dist/archive-zip-manifest.d.ts.map +1 -0
  32. package/dist/archive-zip-manifest.js +22 -0
  33. package/dist/archive-zip-names.d.ts +6 -1
  34. package/dist/archive-zip-names.d.ts.map +1 -1
  35. package/dist/archive-zip-names.js +35 -14
  36. package/dist/archive-zip-preflight.d.ts.map +1 -1
  37. package/dist/archive-zip-preflight.js +3 -2
  38. package/dist/archive.d.ts.map +1 -1
  39. package/dist/archive.js +9 -4
  40. package/dist/copy-file-input.d.ts +1 -1
  41. package/dist/copy-file-input.d.ts.map +1 -1
  42. package/dist/copy-file-input.js +2 -0
  43. package/dist/darwin-acl.d.ts +4 -0
  44. package/dist/darwin-acl.d.ts.map +1 -0
  45. package/dist/darwin-acl.js +24 -0
  46. package/dist/deny-mutations.d.ts.map +1 -1
  47. package/dist/deny-mutations.js +8 -2
  48. package/dist/directory-durability.d.ts.map +1 -1
  49. package/dist/directory-durability.js +67 -23
  50. package/dist/directory-entry-path.d.ts +3 -0
  51. package/dist/directory-entry-path.d.ts.map +1 -0
  52. package/dist/directory-entry-path.js +21 -0
  53. package/dist/directory-guard.d.ts +17 -1
  54. package/dist/directory-guard.d.ts.map +1 -1
  55. package/dist/directory-guard.js +134 -48
  56. package/dist/directory-mode-node.d.ts +12 -0
  57. package/dist/directory-mode-node.d.ts.map +1 -1
  58. package/dist/directory-mode-node.js +102 -4
  59. package/dist/effective-uid.d.ts +2 -0
  60. package/dist/effective-uid.d.ts.map +1 -0
  61. package/dist/effective-uid.js +25 -0
  62. package/dist/file-handle-transfer.d.ts.map +1 -1
  63. package/dist/file-handle-transfer.js +98 -27
  64. package/dist/file-hash.d.ts.map +1 -1
  65. package/dist/file-hash.js +3 -0
  66. package/dist/file-lock-sync.d.ts.map +1 -1
  67. package/dist/file-lock-sync.js +36 -6
  68. package/dist/file-lock.d.ts.map +1 -1
  69. package/dist/file-lock.js +29 -9
  70. package/dist/file-observation.d.ts +1 -1
  71. package/dist/file-observation.d.ts.map +1 -1
  72. package/dist/file-store-boundary.d.ts +6 -2
  73. package/dist/file-store-boundary.d.ts.map +1 -1
  74. package/dist/file-store-boundary.js +20 -65
  75. package/dist/file-store-copy-source.d.ts +5 -0
  76. package/dist/file-store-copy-source.d.ts.map +1 -0
  77. package/dist/file-store-copy-source.js +31 -0
  78. package/dist/file-store-path.d.ts.map +1 -1
  79. package/dist/file-store-path.js +4 -1
  80. package/dist/file-store-sync-directory.d.ts +16 -0
  81. package/dist/file-store-sync-directory.d.ts.map +1 -0
  82. package/dist/file-store-sync-directory.js +349 -0
  83. package/dist/file-store.d.ts.map +1 -1
  84. package/dist/file-store.js +11 -28
  85. package/dist/filename.d.ts.map +1 -1
  86. package/dist/filename.js +65 -20
  87. package/dist/fs.d.ts.map +1 -1
  88. package/dist/fs.js +3 -2
  89. package/dist/guarded-mkdir.d.ts +8 -0
  90. package/dist/guarded-mkdir.d.ts.map +1 -1
  91. package/dist/guarded-mkdir.js +176 -27
  92. package/dist/guest.d.ts.map +1 -1
  93. package/dist/guest.js +4 -1
  94. package/dist/home-dir.d.ts.map +1 -1
  95. package/dist/home-dir.js +73 -10
  96. package/dist/install-path.d.ts.map +1 -1
  97. package/dist/install-path.js +54 -19
  98. package/dist/json-durable-queue-ownership.d.ts +6 -0
  99. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  100. package/dist/json-durable-queue-ownership.js +89 -42
  101. package/dist/json-durable-queue-paths.d.ts +11 -0
  102. package/dist/json-durable-queue-paths.d.ts.map +1 -0
  103. package/dist/json-durable-queue-paths.js +42 -0
  104. package/dist/json-durable-queue-read.d.ts.map +1 -1
  105. package/dist/json-durable-queue-read.js +2 -0
  106. package/dist/json-durable-queue.d.ts.map +1 -1
  107. package/dist/json-durable-queue.js +50 -58
  108. package/dist/json-store.d.ts.map +1 -1
  109. package/dist/json-store.js +5 -1
  110. package/dist/json.d.ts.map +1 -1
  111. package/dist/json.js +54 -22
  112. package/dist/local-file-access.d.ts.map +1 -1
  113. package/dist/local-file-access.js +4 -0
  114. package/dist/local-file-descriptor.d.ts +17 -0
  115. package/dist/local-file-descriptor.d.ts.map +1 -0
  116. package/dist/local-file-descriptor.js +84 -0
  117. package/dist/local-roots.d.ts.map +1 -1
  118. package/dist/local-roots.js +35 -7
  119. package/dist/move-path-cleanup.d.ts +2 -0
  120. package/dist/move-path-cleanup.d.ts.map +1 -1
  121. package/dist/move-path-cleanup.js +44 -18
  122. package/dist/move-path.d.ts.map +1 -1
  123. package/dist/move-path.js +31 -6
  124. package/dist/native-binding.d.ts +17 -0
  125. package/dist/native-binding.d.ts.map +1 -1
  126. package/dist/native-binding.js +8 -1
  127. package/dist/native-directory-observation.d.ts +17 -0
  128. package/dist/native-directory-observation.d.ts.map +1 -0
  129. package/dist/native-directory-observation.js +37 -0
  130. package/dist/native-operations.d.ts.map +1 -1
  131. package/dist/native-operations.js +6 -4
  132. package/dist/native-parent-admission.d.ts +32 -0
  133. package/dist/native-parent-admission.d.ts.map +1 -0
  134. package/dist/native-parent-admission.js +127 -0
  135. package/dist/native-pinned-write-windows.d.ts +0 -1
  136. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  137. package/dist/native-pinned-write-windows.js +8 -9
  138. package/dist/native-pinned-write.d.ts.map +1 -1
  139. package/dist/native-pinned-write.js +314 -42
  140. package/dist/native-staged-file.d.ts +4 -4
  141. package/dist/native-staged-file.d.ts.map +1 -1
  142. package/dist/native-staged-file.js +20 -10
  143. package/dist/native.d.ts +1 -1
  144. package/dist/native.d.ts.map +1 -1
  145. package/dist/native.js +7 -2
  146. package/dist/output.d.ts.map +1 -1
  147. package/dist/output.js +15 -5
  148. package/dist/overwrite-file-handle.d.ts.map +1 -1
  149. package/dist/overwrite-file-handle.js +5 -1
  150. package/dist/owner-dacl.d.ts.map +1 -1
  151. package/dist/owner-dacl.js +2 -0
  152. package/dist/path-policy.d.ts.map +1 -1
  153. package/dist/path-policy.js +7 -2
  154. package/dist/path-prefix.d.ts +7 -0
  155. package/dist/path-prefix.d.ts.map +1 -0
  156. package/dist/path-prefix.js +82 -0
  157. package/dist/path-scope-lexical.d.ts.map +1 -1
  158. package/dist/path-scope-lexical.js +18 -8
  159. package/dist/path-segment-route.d.ts +7 -0
  160. package/dist/path-segment-route.d.ts.map +1 -0
  161. package/dist/path-segment-route.js +24 -0
  162. package/dist/path-suffix-aliases.d.ts +10 -0
  163. package/dist/path-suffix-aliases.d.ts.map +1 -0
  164. package/dist/path-suffix-aliases.js +386 -0
  165. package/dist/path.d.ts.map +1 -1
  166. package/dist/path.js +16 -5
  167. package/dist/permissions-windows.d.ts.map +1 -1
  168. package/dist/permissions-windows.js +14 -3
  169. package/dist/permissions.d.ts.map +1 -1
  170. package/dist/permissions.js +37 -8
  171. package/dist/pinned-mutation-admission.d.ts +25 -0
  172. package/dist/pinned-mutation-admission.d.ts.map +1 -0
  173. package/dist/pinned-mutation-admission.js +425 -0
  174. package/dist/pinned-mutation-observation.d.ts +34 -0
  175. package/dist/pinned-mutation-observation.d.ts.map +1 -0
  176. package/dist/pinned-mutation-observation.js +142 -0
  177. package/dist/pinned-mutation-shared-route.d.ts +24 -0
  178. package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
  179. package/dist/pinned-mutation-shared-route.js +70 -0
  180. package/dist/pinned-open.d.ts +6 -0
  181. package/dist/pinned-open.d.ts.map +1 -1
  182. package/dist/pinned-open.js +28 -10
  183. package/dist/pinned-write-types.d.ts +75 -0
  184. package/dist/pinned-write-types.d.ts.map +1 -0
  185. package/dist/pinned-write-types.js +1 -0
  186. package/dist/pinned-write.d.ts +7 -33
  187. package/dist/pinned-write.d.ts.map +1 -1
  188. package/dist/pinned-write.js +151 -17
  189. package/dist/private-directory.d.ts.map +1 -1
  190. package/dist/private-directory.js +2 -0
  191. package/dist/private-producer-handoff.d.ts +16 -0
  192. package/dist/private-producer-handoff.d.ts.map +1 -0
  193. package/dist/private-producer-handoff.js +272 -0
  194. package/dist/private-temp-workspace.d.ts +2 -39
  195. package/dist/private-temp-workspace.d.ts.map +1 -1
  196. package/dist/private-temp-workspace.js +183 -77
  197. package/dist/publish-file.d.ts.map +1 -1
  198. package/dist/publish-file.js +38 -32
  199. package/dist/regular-file.d.ts.map +1 -1
  200. package/dist/regular-file.js +56 -45
  201. package/dist/replace-directory.d.ts.map +1 -1
  202. package/dist/replace-directory.js +12 -5
  203. package/dist/replace-file-temp-owner.d.ts +1 -0
  204. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  205. package/dist/replace-file-temp-owner.js +12 -1
  206. package/dist/replace-file.d.ts.map +1 -1
  207. package/dist/replace-file.js +24 -24
  208. package/dist/root-boundary.d.ts +4 -0
  209. package/dist/root-boundary.d.ts.map +1 -1
  210. package/dist/root-boundary.js +6 -1
  211. package/dist/root-context.d.ts +12 -1
  212. package/dist/root-context.d.ts.map +1 -1
  213. package/dist/root-context.js +57 -11
  214. package/dist/root-directory-creation.d.ts +13 -0
  215. package/dist/root-directory-creation.d.ts.map +1 -0
  216. package/dist/root-directory-creation.js +212 -0
  217. package/dist/root-directory-list.d.ts +16 -4
  218. package/dist/root-directory-list.d.ts.map +1 -1
  219. package/dist/root-directory-list.js +180 -39
  220. package/dist/root-directory.d.ts +23 -0
  221. package/dist/root-directory.d.ts.map +1 -0
  222. package/dist/root-directory.js +141 -0
  223. package/dist/root-errors.d.ts.map +1 -1
  224. package/dist/root-errors.js +12 -1
  225. package/dist/root-file-final-admission.d.ts +21 -0
  226. package/dist/root-file-final-admission.d.ts.map +1 -0
  227. package/dist/root-file-final-admission.js +83 -0
  228. package/dist/root-file.d.ts.map +1 -1
  229. package/dist/root-file.js +103 -24
  230. package/dist/root-impl.d.ts +1 -0
  231. package/dist/root-impl.d.ts.map +1 -1
  232. package/dist/root-impl.js +289 -317
  233. package/dist/root-move-noreplace.d.ts +14 -0
  234. package/dist/root-move-noreplace.d.ts.map +1 -0
  235. package/dist/root-move-noreplace.js +202 -0
  236. package/dist/root-observed-path.d.ts +9 -0
  237. package/dist/root-observed-path.d.ts.map +1 -0
  238. package/dist/root-observed-path.js +95 -0
  239. package/dist/root-path-errors.d.ts +12 -0
  240. package/dist/root-path-errors.d.ts.map +1 -0
  241. package/dist/root-path-errors.js +13 -0
  242. package/dist/root-path-existing.d.ts.map +1 -1
  243. package/dist/root-path-existing.js +53 -20
  244. package/dist/root-path-observation.d.ts +63 -0
  245. package/dist/root-path-observation.d.ts.map +1 -0
  246. package/dist/root-path-observation.js +180 -0
  247. package/dist/root-path-stat.d.ts +5 -0
  248. package/dist/root-path-stat.d.ts.map +1 -0
  249. package/dist/root-path-stat.js +101 -0
  250. package/dist/root-path-symlink.d.ts.map +1 -1
  251. package/dist/root-path-symlink.js +23 -4
  252. package/dist/root-path.d.ts +11 -0
  253. package/dist/root-path.d.ts.map +1 -1
  254. package/dist/root-path.js +215 -53
  255. package/dist/root-paths-lexical.d.ts +9 -0
  256. package/dist/root-paths-lexical.d.ts.map +1 -0
  257. package/dist/root-paths-lexical.js +22 -0
  258. package/dist/root-paths.d.ts +3 -25
  259. package/dist/root-paths.d.ts.map +1 -1
  260. package/dist/root-paths.js +77 -154
  261. package/dist/root-read-admission.d.ts +26 -0
  262. package/dist/root-read-admission.d.ts.map +1 -0
  263. package/dist/root-read-admission.js +94 -0
  264. package/dist/root-remove-identity.d.ts +15 -0
  265. package/dist/root-remove-identity.d.ts.map +1 -0
  266. package/dist/root-remove-identity.js +89 -0
  267. package/dist/root-remove-receipt.d.ts +17 -0
  268. package/dist/root-remove-receipt.d.ts.map +1 -0
  269. package/dist/root-remove-receipt.js +37 -0
  270. package/dist/root-remove.d.ts +2 -1
  271. package/dist/root-remove.d.ts.map +1 -1
  272. package/dist/root-remove.js +172 -10
  273. package/dist/root-write-admission.d.ts +68 -0
  274. package/dist/root-write-admission.d.ts.map +1 -0
  275. package/dist/root-write-admission.js +322 -0
  276. package/dist/root-write-compatibility.d.ts +13 -0
  277. package/dist/root-write-compatibility.d.ts.map +1 -0
  278. package/dist/root-write-compatibility.js +74 -0
  279. package/dist/root-write-complete-parent.d.ts +42 -0
  280. package/dist/root-write-complete-parent.d.ts.map +1 -0
  281. package/dist/root-write-complete-parent.js +195 -0
  282. package/dist/root-write-publication.d.ts +24 -0
  283. package/dist/root-write-publication.d.ts.map +1 -0
  284. package/dist/root-write-publication.js +85 -0
  285. package/dist/root-write-verification.d.ts.map +1 -1
  286. package/dist/root-write-verification.js +6 -4
  287. package/dist/secret-file.d.ts.map +1 -1
  288. package/dist/secret-file.js +62 -24
  289. package/dist/secret-read-async.d.ts.map +1 -1
  290. package/dist/secret-read-async.js +18 -13
  291. package/dist/secret-read-policy.d.ts.map +1 -1
  292. package/dist/secret-read-policy.js +5 -1
  293. package/dist/secure-file.d.ts.map +1 -1
  294. package/dist/secure-file.js +92 -7
  295. package/dist/secure-temp-dir.d.ts +6 -3
  296. package/dist/secure-temp-dir.d.ts.map +1 -1
  297. package/dist/secure-temp-dir.js +121 -101
  298. package/dist/secure-temp-repair.d.ts +35 -0
  299. package/dist/secure-temp-repair.d.ts.map +1 -0
  300. package/dist/secure-temp-repair.js +106 -0
  301. package/dist/sibling-staged-file.d.ts +3 -2
  302. package/dist/sibling-staged-file.d.ts.map +1 -1
  303. package/dist/sibling-staged-file.js +179 -55
  304. package/dist/sibling-temp.d.ts.map +1 -1
  305. package/dist/sibling-temp.js +27 -13
  306. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  307. package/dist/sidecar-lock-acquire.js +44 -14
  308. package/dist/sidecar-lock-root.d.ts.map +1 -1
  309. package/dist/sidecar-lock-root.js +4 -2
  310. package/dist/staged-directory.d.ts +14 -5
  311. package/dist/staged-directory.d.ts.map +1 -1
  312. package/dist/staged-directory.js +54 -3
  313. package/dist/standalone-publication-path.d.ts +2 -0
  314. package/dist/standalone-publication-path.d.ts.map +1 -0
  315. package/dist/standalone-publication-path.js +6 -0
  316. package/dist/stat-observation.d.ts +11 -0
  317. package/dist/stat-observation.d.ts.map +1 -0
  318. package/dist/stat-observation.js +64 -0
  319. package/dist/strict-file-identity.d.ts.map +1 -1
  320. package/dist/strict-file-identity.js +39 -2
  321. package/dist/symlink-parents.d.ts.map +1 -1
  322. package/dist/symlink-parents.js +7 -2
  323. package/dist/temp-target.d.ts +2 -0
  324. package/dist/temp-target.d.ts.map +1 -1
  325. package/dist/temp-target.js +130 -3
  326. package/dist/temp-workspace-admission.d.ts +22 -0
  327. package/dist/temp-workspace-admission.d.ts.map +1 -0
  328. package/dist/temp-workspace-admission.js +374 -0
  329. package/dist/temp-workspace-child-admission.d.ts +13 -0
  330. package/dist/temp-workspace-child-admission.d.ts.map +1 -0
  331. package/dist/temp-workspace-child-admission.js +95 -0
  332. package/dist/temp-workspace-descriptor.d.ts +48 -0
  333. package/dist/temp-workspace-descriptor.d.ts.map +1 -0
  334. package/dist/temp-workspace-descriptor.js +363 -0
  335. package/dist/temp-workspace-identity.d.ts +15 -0
  336. package/dist/temp-workspace-identity.d.ts.map +1 -0
  337. package/dist/temp-workspace-identity.js +41 -0
  338. package/dist/temp-workspace-owner.d.ts +7 -6
  339. package/dist/temp-workspace-owner.d.ts.map +1 -1
  340. package/dist/temp-workspace-owner.js +121 -61
  341. package/dist/temp-workspace-permissions.d.ts +4 -0
  342. package/dist/temp-workspace-permissions.d.ts.map +1 -0
  343. package/dist/temp-workspace-permissions.js +32 -0
  344. package/dist/temp-workspace-types.d.ts +40 -0
  345. package/dist/temp-workspace-types.d.ts.map +1 -0
  346. package/dist/temp-workspace-types.js +1 -0
  347. package/dist/temp.d.ts +1 -1
  348. package/dist/temp.d.ts.map +1 -1
  349. package/dist/temp.js +1 -1
  350. package/dist/test-hooks.d.ts +7 -0
  351. package/dist/test-hooks.d.ts.map +1 -1
  352. package/dist/text-atomic.d.ts.map +1 -1
  353. package/dist/text-atomic.js +3 -1
  354. package/dist/trash.d.ts.map +1 -1
  355. package/dist/trash.js +54 -8
  356. package/dist/walk.d.ts.map +1 -1
  357. package/dist/walk.js +9 -6
  358. package/dist/windows-owner.d.ts.map +1 -1
  359. package/dist/windows-owner.js +5 -0
  360. package/dist/windows-path-alias.d.ts +39 -0
  361. package/dist/windows-path-alias.d.ts.map +1 -0
  362. package/dist/windows-path-alias.js +153 -0
  363. package/docs/advanced.md +25 -0
  364. package/docs/archive.md +23 -2
  365. package/docs/atomic.md +27 -3
  366. package/docs/copy.md +3 -0
  367. package/docs/durability.md +14 -0
  368. package/docs/errors.md +14 -6
  369. package/docs/file-store.md +24 -3
  370. package/docs/filename.md +14 -7
  371. package/docs/install-path.md +2 -2
  372. package/docs/install.md +8 -7
  373. package/docs/json-store.md +5 -1
  374. package/docs/json.md +11 -0
  375. package/docs/mutation-policy-proof.md +65 -0
  376. package/docs/native-helper.md +41 -11
  377. package/docs/native.md +32 -8
  378. package/docs/output.md +33 -11
  379. package/docs/path-prefix.md +64 -0
  380. package/docs/path-suffix-aliases.md +159 -0
  381. package/docs/path.md +11 -0
  382. package/docs/private-file-store.md +14 -0
  383. package/docs/public-api.md +9 -0
  384. package/docs/quickstart.md +1 -1
  385. package/docs/reading.md +8 -1
  386. package/docs/root.md +18 -3
  387. package/docs/secret-file.md +3 -0
  388. package/docs/secure-file.md +6 -2
  389. package/docs/security-model.md +75 -8
  390. package/docs/sidecar-lock.md +28 -1
  391. package/docs/store.md +5 -1
  392. package/docs/temp.md +260 -36
  393. package/docs/test-hooks.md +14 -0
  394. package/docs/writing.md +38 -8
  395. package/package.json +10 -10
@@ -5,38 +5,43 @@ import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
5
5
  import { FsSafeError } from "./errors.js";
6
6
  import { resolveReadOpenFlags } from "./read-open-flags.js";
7
7
  import { realpathSync } from "./realpath.js";
8
- import { inspectFileIdentity } from "./strict-file-identity.js";
8
+ import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
9
9
  import { assertSecretFilePreview, resolveSecretReadPolicy, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
10
+ import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
10
11
  export async function readSecretFile(filePath, label, options = {}) {
11
12
  const { resolvedPath, maxBytes } = resolveSecretReadPolicy(filePath, label, options);
12
- async function inspectInput(symlinkMessage) {
13
- const stat = options.rejectSymlink
14
- ? fsSync.lstatSync(resolvedPath, { bigint: true })
15
- : fsSync.statSync(resolvedPath, { bigint: true });
16
- if (options.rejectSymlink && stat.isSymbolicLink()) {
17
- throw new FsSafeError("symlink", symlinkMessage);
18
- }
19
- return stat;
20
- }
13
+ let rejectSymlink;
21
14
  let previewStat;
22
15
  try {
23
16
  assertNoUnsafeDeviceReadPath(resolvedPath);
24
- previewStat = await inspectFileIdentity(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
17
+ rejectSymlink = Boolean(options.rejectSymlink);
18
+ previewStat = inspectFileIdentitySync(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
25
19
  }
26
20
  catch (error) {
27
21
  throw secretReadError(error instanceof FsSafeError ? error.code : secretPathErrorCode(error), "inspect", label, resolvedPath, error);
28
22
  }
29
- assertSecretFilePreview(previewStat, label, resolvedPath, maxBytes, options.rejectHardlinks !== false);
23
+ function inspectInput(symlinkMessage) {
24
+ const stat = rejectSymlink
25
+ ? fsSync.lstatSync(resolvedPath, { bigint: true })
26
+ : fsSync.statSync(resolvedPath, { bigint: true });
27
+ if (rejectSymlink && stat.isSymbolicLink()) {
28
+ throw new FsSafeError("symlink", symlinkMessage);
29
+ }
30
+ return stat;
31
+ }
32
+ const rejectHardlinks = options.rejectHardlinks !== false;
33
+ assertSecretFilePreview(previewStat, label, resolvedPath, maxBytes, rejectHardlinks);
30
34
  let handle;
31
35
  let raw;
32
36
  try {
33
37
  const realPath = realpathSync.native(resolvedPath);
38
+ assertNoWindowsPathAlias(realPath, "filesystem", `${label} file path uses a Windows filesystem namespace alias`);
34
39
  assertNoUnsafeDeviceReadPath(realPath);
35
40
  handle = await fs.open(realPath, resolveReadOpenFlags());
36
41
  const openedHandle = handle;
37
42
  const openedStat = await inspectFileIdentity(async () => {
38
43
  const stat = fsSync.fstatSync(openedHandle.fd, { bigint: true });
39
- if (!stat.isFile() || (options.rejectHardlinks !== false && stat.nlink > 1n)) {
44
+ if (!stat.isFile() || (rejectHardlinks && stat.nlink > 1n)) {
40
45
  throw new FsSafeError("path-mismatch", "security validation failed");
41
46
  }
42
47
  return stat;
@@ -1 +1 @@
1
- {"version":3,"file":"secret-read-policy.d.ts","sourceRoot":"","sources":["../src/secret-read-policy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAEhE,eAAO,MAAM,6BAA6B,QAAY,CAAC;AAEvD,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAGnE;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,GAAG,MAAM,EAC1B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,OAAO,GACb,WAAW,CAGb;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,OAAO,GACvB,IAAI,CAUN;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAM9F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG;IACxG,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB,CASA"}
1
+ {"version":3,"file":"secret-read-policy.d.ts","sourceRoot":"","sources":["../src/secret-read-policy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAGhE,eAAO,MAAM,6BAA6B,QAAY,CAAC;AAEvD,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAGnE;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,GAAG,MAAM,EAC1B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,OAAO,GACb,WAAW,CAGb;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,OAAO,GACvB,IAAI,CAUN;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAM9F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG;IACxG,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAYA"}
@@ -1,6 +1,7 @@
1
1
  import { normalizeMaxBytes } from "./byte-budget.js";
2
2
  import { resolveHomeRelativePath } from "./home-dir.js";
3
3
  import { FsSafeError } from "./errors.js";
4
+ import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
4
5
  export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
5
6
  export function secretPathErrorCode(error) {
6
7
  const code = error.code;
@@ -29,10 +30,13 @@ export function trimSecretFileContent(raw, label, resolvedPath) {
29
30
  return secret;
30
31
  }
31
32
  export function resolveSecretReadPolicy(filePath, label, options) {
32
- const resolvedPath = resolveHomeRelativePath(filePath.trim());
33
+ const trimmedPath = filePath.trim();
34
+ assertNoWindowsPathAlias(trimmedPath, "filesystem", `${label} file path uses a Windows filesystem namespace alias`);
35
+ const resolvedPath = resolveHomeRelativePath(trimmedPath);
33
36
  if (!resolvedPath) {
34
37
  throw new FsSafeError("invalid-path", `${label} file path is empty.`, { cause: undefined });
35
38
  }
39
+ assertNoWindowsPathAlias(resolvedPath, "filesystem", `${label} file path uses a Windows filesystem namespace alias`);
36
40
  const maxBytes = normalizeMaxBytes(options.maxBytes, {
37
41
  defaultValue: DEFAULT_SECRET_FILE_MAX_BYTES,
38
42
  });
@@ -1 +1 @@
1
- {"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAc/D,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAK1B,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,WAAW,CAAC,EAAE,2BAA2B,CAAC;IAC1C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,EAAE,CAAC,EAAE,mBAAmB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,CAAC;AAE7D,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAuNF,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CA6B/B"}
1
+ {"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAe/D,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAU1B,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,WAAW,CAAC,EAAE,2BAA2B,CAAC;IAC1C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,EAAE,CAAC,EAAE,mBAAmB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,CAAC;AAE7D,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAgTF,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAgC/B"}
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { readFileHandleBounded } from "./bounded-read.js";
5
5
  import { normalizeMaxBytes } from "./byte-budget.js";
6
6
  import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
7
+ import { resolveEffectiveUid } from "./effective-uid.js";
7
8
  import { FsSafeError } from "./errors.js";
8
9
  import { isWindowsDriveLetterPath, isWindowsNetworkPath } from "./local-file-access.js";
9
10
  import { isPathInside, isSymlinkOpenError } from "./path.js";
@@ -14,6 +15,75 @@ import { inspectPathPermissions, isGroupReadable, isGroupWritable, isWorldReadab
14
15
  import { inspectFileIdentity } from "./strict-file-identity.js";
15
16
  import { inspectSecureWindowsDescriptor } from "./secure-file-windows.js";
16
17
  import { scheduleTimeout } from "./timing.js";
18
+ import { anchorWindowsDriveRelativePath, assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
19
+ function snapshotTrustedDirs(dirs) {
20
+ if (dirs === undefined)
21
+ return undefined;
22
+ if (!Array.isArray(dirs)) {
23
+ throw new FsSafeError("invalid-path", "trustedDirs must be an array of path strings.");
24
+ }
25
+ const trustedDirs = [];
26
+ const length = dirs.length;
27
+ if (!Number.isSafeInteger(length) || length < 0 || length > 0xffff_ffff) {
28
+ throw new FsSafeError("invalid-path", "trustedDirs must have a valid array length.");
29
+ }
30
+ for (let index = 0; index < length; index += 1) {
31
+ if (!Object.hasOwn(dirs, index)) {
32
+ throw new FsSafeError("invalid-path", "trustedDirs must not contain sparse entries.");
33
+ }
34
+ const dir = dirs[index];
35
+ if (typeof dir !== "string" || dir.includes("\0")) {
36
+ throw new FsSafeError("invalid-path", "trustedDirs must contain only path strings without null bytes.");
37
+ }
38
+ // Admit the raw suffix before normalization can erase an alternate stream,
39
+ // while retaining Node's per-drive cwd semantics for D:credentials.
40
+ const anchored = anchorWindowsDriveRelativePath(dir);
41
+ assertNoWindowsPathAlias(anchored, "filesystem", "trusted directory uses a Windows filesystem namespace alias");
42
+ const resolved = resolvePathPreservingWindowsRoot(anchored);
43
+ assertNoWindowsPathAlias(resolved, "filesystem", "trusted directory uses a Windows filesystem namespace alias");
44
+ trustedDirs.push(resolved);
45
+ }
46
+ return trustedDirs;
47
+ }
48
+ function snapshotPermissionEnv(env) {
49
+ if (env === undefined)
50
+ return undefined;
51
+ const snapshot = { ...env };
52
+ // Windows command lookup first reads these exact names, even when inherited
53
+ // or non-enumerable, then considers only enumerable own case variants.
54
+ // Reuse copied values so enumerable getters are not invoked a second time.
55
+ if (!Object.hasOwn(snapshot, "SystemRoot"))
56
+ snapshot.SystemRoot = env.SystemRoot;
57
+ if (!Object.hasOwn(snapshot, "WINDIR"))
58
+ snapshot.WINDIR = env.WINDIR;
59
+ return snapshot;
60
+ }
61
+ function snapshotSecureFileOptions(options, io, maxBytes) {
62
+ const { filePath, label, trust, permissions, inject } = options;
63
+ const env = inject?.env;
64
+ return {
65
+ filePath,
66
+ label,
67
+ trust: {
68
+ trustedDirs: snapshotTrustedDirs(trust?.trustedDirs),
69
+ allowSymlink: trust?.allowSymlink,
70
+ allowNetworkPath: trust?.allowNetworkPath,
71
+ },
72
+ permissions: {
73
+ allowInsecure: permissions?.allowInsecure,
74
+ allowReadableByOthers: permissions?.allowReadableByOthers,
75
+ },
76
+ inject: {
77
+ platform: inject?.platform,
78
+ env: snapshotPermissionEnv(env),
79
+ exec: inject?.exec,
80
+ },
81
+ io: {
82
+ maxBytes,
83
+ timeoutMs: io?.timeoutMs,
84
+ },
85
+ };
86
+ }
17
87
  function isAbsolutePathname(value) {
18
88
  return (path.isAbsolute(value) ||
19
89
  (process.platform === "win32" &&
@@ -82,6 +152,7 @@ async function openSecureHandle(options, maxBytes) {
82
152
  return pathStat;
83
153
  }, openedIdentity);
84
154
  const realPath = realpathSync.native(options.filePath);
155
+ assertNoWindowsPathAlias(realPath, "filesystem", `${label(options)} resolved path uses a Windows filesystem namespace alias`);
85
156
  await inspectFileIdentity(() => {
86
157
  const realPathStat = fsSync.statSync(realPath, { bigint: true });
87
158
  assertNotHardlinked(options, realPathStat);
@@ -102,13 +173,15 @@ async function assertTrustedDirs(options, realPath) {
102
173
  return;
103
174
  }
104
175
  const trusted = await Promise.all(options.trust.trustedDirs.map(async (dir) => {
105
- const resolved = path.resolve(dir);
176
+ let realPath;
106
177
  try {
107
- return realpathSync.native(resolved);
178
+ realPath = realpathSync.native(dir);
108
179
  }
109
180
  catch {
110
- return resolved;
181
+ return dir;
111
182
  }
183
+ assertNoWindowsPathAlias(realPath, "filesystem", "trusted directory uses a Windows filesystem namespace alias");
184
+ return realPath;
112
185
  }));
113
186
  if (!trusted.some((dir) => isPathInside(dir, realPath))) {
114
187
  throw new FsSafeError("outside-workspace", `${label(options)} is outside trustedDirs: ${realPath}`);
@@ -163,10 +236,19 @@ async function assertSecurePermissions(options, stat, realPath, identity, fd) {
163
236
  if (writableByOthers || (!options.permissions?.allowReadableByOthers && readableByOthers)) {
164
237
  throw new FsSafeError("insecure-permissions", `${label(options)} permissions are too open: ${realPath}`);
165
238
  }
166
- if (platform !== "win32" && typeof process.getuid === "function" && stat.uid != null) {
167
- const uid = process.getuid();
239
+ if (platform !== "win32") {
240
+ let uid;
241
+ try {
242
+ uid = resolveEffectiveUid();
243
+ }
244
+ catch (cause) {
245
+ throw new FsSafeError("permission-unverified", `${label(options)} owner identity could not be verified for the effective user: ${realPath}`, { cause });
246
+ }
247
+ if (uid === undefined || !Number.isSafeInteger(stat.uid) || stat.uid < 0) {
248
+ throw new FsSafeError("permission-unverified", `${label(options)} owner identity could not be verified for the effective user: ${realPath}`);
249
+ }
168
250
  if (stat.uid !== uid) {
169
- throw new FsSafeError("not-owned", `${label(options)} must be owned by the current user (uid=${uid}): ${realPath}`);
251
+ throw new FsSafeError("not-owned", `${label(options)} must be owned by the effective user (uid=${uid}): ${realPath}`);
170
252
  }
171
253
  }
172
254
  return permissions;
@@ -193,7 +275,10 @@ async function readHandleWithTimeout(handle, timeoutMs, maxBytes) {
193
275
  }
194
276
  }
195
277
  export async function readSecureFile(options) {
196
- const maxBytes = normalizeMaxBytes(options.io?.maxBytes);
278
+ const io = options.io;
279
+ const maxBytes = normalizeMaxBytes(io?.maxBytes);
280
+ options = snapshotSecureFileOptions(options, io, maxBytes);
281
+ assertNoWindowsPathAlias(options.filePath, "filesystem", `${label(options)} path uses a Windows filesystem namespace alias`);
197
282
  const opened = await openSecureHandle(options, maxBytes);
198
283
  try {
199
284
  await assertTrustedDirs(options, opened.realPath);
@@ -1,12 +1,16 @@
1
+ import { type SecureTempRootDescriptorAdapter } from "./secure-temp-repair.js";
2
+ export type { SecureTempRootDescriptorAdapter } from "./secure-temp-repair.js";
1
3
  type SecureDirStat = {
2
4
  isDirectory(): boolean;
3
5
  isSymbolicLink(): boolean;
4
- mode?: number;
5
- uid?: number;
6
+ mode?: number | bigint;
7
+ uid?: number | bigint;
6
8
  };
7
9
  export type ResolveSecureTempRootOptions = {
8
10
  accessSync?: (path: string, mode?: number) => void;
11
+ /** @deprecated Unused. Repairs require descriptor-bound chmod. */
9
12
  chmodSync?: (path: string, mode: number) => void;
13
+ descriptor?: SecureTempRootDescriptorAdapter;
10
14
  fallbackPrefix: string;
11
15
  getuid?: () => number | undefined;
12
16
  lstatSync?: (path: string) => SecureDirStat;
@@ -23,5 +27,4 @@ export type ResolveSecureTempRootOptions = {
23
27
  warningPrefix?: string;
24
28
  };
25
29
  export declare function resolveSecureTempRoot(options: ResolveSecureTempRootOptions): string;
26
- export {};
27
30
  //# sourceMappingURL=secure-temp-dir.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"secure-temp-dir.d.ts","sourceRoot":"","sources":["../src/secure-temp-dir.ts"],"names":[],"mappings":"AAQA,KAAK,aAAa,GAAG;IACnB,WAAW,IAAI,OAAO,CAAC;IACvB,cAAc,IAAI,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,aAAa,CAAC;IAC5C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAWF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,MAAM,CA+JnF"}
1
+ {"version":3,"file":"secure-temp-dir.d.ts","sourceRoot":"","sources":["../src/secure-temp-dir.ts"],"names":[],"mappings":"AAOA,OAAO,EAKL,KAAK,+BAA+B,EACrC,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,+BAA+B,EAAE,MAAM,yBAAyB,CAAC;AAI/E,KAAK,aAAa,GAAG;IACnB,WAAW,IAAI,OAAO,CAAC;IACvB,cAAc,IAAI,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,kEAAkE;IAClE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,UAAU,CAAC,EAAE,+BAA+B,CAAC;IAC7C,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,aAAa,CAAC;IAC5C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAWF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,MAAM,CA6LnF"}
@@ -1,8 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import { tmpdir as getOsTmpDir } from "node:os";
3
3
  import path from "node:path";
4
+ import { directoryEntryPath } from "./directory-entry-path.js";
4
5
  import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
5
6
  import { assertSafePathSegment } from "./safe-path-segment.js";
7
+ import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-path-alias.js";
8
+ import { captureSecureTempRepairAdapter, repairSecureTempDirectory, secureTempDirectoryReceipt, } from "./secure-temp-repair.js";
6
9
  function isNodeErrorWithCode(err, code) {
7
10
  return (typeof err === "object" &&
8
11
  err !== null &&
@@ -10,152 +13,169 @@ function isNodeErrorWithCode(err, code) {
10
13
  err.code === code);
11
14
  }
12
15
  export function resolveSecureTempRoot(options) {
13
- const fallbackPrefix = assertSafePathSegment(options.fallbackPrefix, {
16
+ const { fallbackPrefix: prefix, accessSync: suppliedAccess, lstatSync: suppliedLstat, mkdirSync: suppliedMkdir, descriptor: suppliedDescriptor, getuid: suppliedGetuid, tmpdir: suppliedTmpdir, platform: suppliedPlatform, preferredDir, skipPreferredOnWindows, warn: suppliedWarn, warningPrefix: suppliedWarningPrefix, unsafeFallbackLabel: suppliedFallbackLabel, } = options;
17
+ const platform = suppliedPlatform ?? process.platform;
18
+ const windows = process.platform === "win32" || platform === "win32";
19
+ // A platform adapter cannot disable the host's Windows pathname policy.
20
+ const admissionPlatform = process.platform === "win32" ? process.platform : platform;
21
+ const capturedDescriptor = captureSecureTempRepairAdapter(suppliedDescriptor, suppliedAccess !== undefined || suppliedLstat !== undefined || suppliedMkdir !== undefined);
22
+ const descriptor = windows ? undefined : capturedDescriptor;
23
+ const hostLstat = fs.lstatSync;
24
+ const hostAccess = fs.accessSync;
25
+ const hostMkdir = fs.mkdirSync;
26
+ const hostGetuid = process.getuid;
27
+ const accessSync = suppliedAccess ?? (process.platform === "win32"
28
+ ? (candidate, mode) => hostAccess(pathForWindowsFilesystem(candidate), mode)
29
+ : hostAccess);
30
+ const lstatSync = descriptor
31
+ ? (candidate) => descriptor.lstatSync(candidate, { bigint: true })
32
+ : suppliedLstat ?? ((candidate) => windows
33
+ ? hostLstat(pathForWindowsFilesystem(candidate))
34
+ : hostLstat(candidate, { bigint: true }));
35
+ // Virtual observations cannot authorize host creation. A supplied mkdir hook
36
+ // is explicit authority; chmodSync is deprecated and deliberately not read.
37
+ const mkdirSync = suppliedMkdir ?? (suppliedLstat !== undefined || suppliedAccess !== undefined || suppliedDescriptor !== undefined
38
+ ? undefined
39
+ : (directory, mkdirOptions) => hostMkdir(recursiveMkdirPath(pathForWindowsFilesystem(directory)), mkdirOptions));
40
+ const warn = suppliedWarn ?? console.warn.bind(console);
41
+ const warningPrefix = suppliedWarningPrefix ?? "[fs-safe]";
42
+ const unsafeFallbackLabel = suppliedFallbackLabel ?? "secure temp dir";
43
+ const tmpdir = typeof suppliedTmpdir === "function" ? suppliedTmpdir : getOsTmpDir;
44
+ const getuid = suppliedGetuid ?? (() => {
45
+ try {
46
+ return typeof hostGetuid === "function" ? hostGetuid.call(process) : undefined;
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ });
52
+ const fallbackPrefix = assertSafePathSegment(prefix, {
14
53
  allowDotPrefix: true,
15
54
  label: "fallback temp prefix",
16
55
  });
17
56
  const TMP_DIR_ACCESS_MODE = fs.constants.W_OK | fs.constants.X_OK;
18
- const accessSync = options.accessSync ?? fs.accessSync;
19
- const chmodSync = options.chmodSync ?? fs.chmodSync;
20
- const lstatSync = options.lstatSync ?? fs.lstatSync;
21
- const mkdirSync = options.mkdirSync ?? ((directory, mkdirOptions) => fs.mkdirSync(recursiveMkdirPath(directory), mkdirOptions));
22
- const warn = options.warn ?? ((message) => console.warn(message));
23
- const warningPrefix = options.warningPrefix ?? "[fs-safe]";
24
- const unsafeFallbackLabel = options.unsafeFallbackLabel ?? "secure temp dir";
25
- const getuid = options.getuid ??
26
- (() => {
27
- try {
28
- return typeof process.getuid === "function" ? process.getuid() : undefined;
29
- }
30
- catch {
31
- return undefined;
32
- }
33
- });
34
- const tmpdir = typeof options.tmpdir === "function" ? options.tmpdir : getOsTmpDir;
35
- const platform = options.platform ?? process.platform;
36
57
  const uid = getuid();
58
+ if (uid !== undefined && (!Number.isSafeInteger(uid) || uid < 0)) {
59
+ throw new Error("Secure temp directory user identity is invalid.");
60
+ }
61
+ if (preferredDir !== undefined) {
62
+ assertNoWindowsPathAlias(preferredDir, "filesystem", "preferred temp directory uses a Windows filesystem namespace alias", admissionPlatform);
63
+ }
37
64
  const isSecureDirForUser = (st) => {
38
- if (uid === undefined) {
65
+ if (windows || uid === undefined)
39
66
  return true;
40
- }
41
- if (typeof st.uid === "number" && st.uid !== uid) {
67
+ const { uid: owner, mode } = st;
68
+ if (owner !== undefined && owner !== uid && owner !== BigInt(uid))
42
69
  return false;
43
- }
44
- if (typeof st.mode === "number" && (st.mode & 0o022) !== 0) {
45
- return false;
46
- }
47
- return true;
70
+ if (typeof mode === "bigint")
71
+ return mode >= 0n && (mode & 18n) === 0n;
72
+ return mode === undefined || (Number.isSafeInteger(mode) && mode >= 0 && (mode & 0o022) === 0);
48
73
  };
49
74
  const fallback = () => {
50
75
  const base = tmpdir();
76
+ assertNoWindowsPathAlias(base, "filesystem", "system temp directory uses a Windows filesystem namespace alias", admissionPlatform);
51
77
  const suffix = uid === undefined ? fallbackPrefix : `${fallbackPrefix}-${uid}`;
52
78
  const joiner = platform === "win32" ? path.win32.join : path.join;
53
- return joiner(base, suffix);
54
- };
55
- const isTrustedTmpDir = (st) => {
56
- return st.isDirectory() && !st.isSymbolicLink() && isSecureDirForUser(st);
79
+ const fallbackPath = joiner(base, suffix);
80
+ assertNoWindowsPathAlias(fallbackPath, "filesystem", "fallback temp directory uses a Windows filesystem namespace alias", admissionPlatform);
81
+ return fallbackPath;
57
82
  };
58
83
  const resolveDirState = (candidatePath) => {
84
+ assertNoWindowsPathAlias(candidatePath, "filesystem", "temp directory uses a Windows filesystem namespace alias", admissionPlatform);
85
+ let candidate;
59
86
  try {
60
- const candidate = lstatSync(candidatePath);
61
- if (!isTrustedTmpDir(candidate)) {
62
- return "invalid";
87
+ candidate = lstatSync(candidatePath);
88
+ }
89
+ catch (error) {
90
+ return { kind: isNodeErrorWithCode(error, "ENOENT") ? "missing" : "invalid", error };
91
+ }
92
+ let receipt;
93
+ try {
94
+ if (descriptor && uid !== undefined) {
95
+ receipt = secureTempDirectoryReceipt(candidate, uid);
96
+ }
97
+ if (receipt ? (receipt.mode & 18n) !== 0n :
98
+ candidate.isDirectory() !== true || candidate.isSymbolicLink() !== false || !isSecureDirForUser(candidate)) {
99
+ return { kind: "invalid", receipt };
63
100
  }
64
101
  accessSync(candidatePath, TMP_DIR_ACCESS_MODE);
65
- return "available";
102
+ return { kind: "available", receipt };
66
103
  }
67
- catch (err) {
68
- if (isNodeErrorWithCode(err, "ENOENT")) {
69
- return "missing";
70
- }
71
- return "invalid";
104
+ catch (error) {
105
+ return { kind: "invalid", receipt, error };
72
106
  }
73
107
  };
74
- const tryRepairWritableBits = (candidatePath) => {
108
+ const tryRepair = (candidatePath, state, finalize = false) => {
109
+ assertNoWindowsPathAlias(candidatePath, "filesystem", "temp directory uses a Windows filesystem namespace alias", admissionPlatform);
110
+ if (!descriptor || uid === undefined || !state.receipt) {
111
+ return finalize ? { kind: "invalid", error: new Error("Secure temp descriptor finalization is unavailable.") } : state;
112
+ }
75
113
  try {
76
- const st = lstatSync(candidatePath);
77
- if (!st.isDirectory() || st.isSymbolicLink()) {
78
- return false;
79
- }
80
- if (uid !== undefined && typeof st.uid === "number" && st.uid !== uid) {
81
- return false;
82
- }
83
- if (typeof st.mode !== "number") {
84
- return false;
85
- }
86
- if ((st.mode & 0o022) === 0) {
87
- return resolveDirState(candidatePath) === "available";
88
- }
89
- try {
90
- chmodSync(candidatePath, 0o700);
91
- }
92
- catch (chmodErr) {
93
- if (isNodeErrorWithCode(chmodErr, "EPERM") ||
94
- isNodeErrorWithCode(chmodErr, "EACCES") ||
95
- isNodeErrorWithCode(chmodErr, "ENOENT")) {
96
- return resolveDirState(candidatePath) === "available";
97
- }
98
- throw chmodErr;
99
- }
100
- warn(`${warningPrefix} tightened permissions on temp dir: ${candidatePath}`);
101
- return resolveDirState(candidatePath) === "available";
114
+ repairSecureTempDirectory(candidatePath, state.receipt, uid, descriptor, () => accessSync(candidatePath, TMP_DIR_ACCESS_MODE), () => warn(`${warningPrefix} tightened permissions on temp dir: ${candidatePath}`), finalize);
115
+ return { kind: "available" };
102
116
  }
103
- catch {
104
- return false;
117
+ catch (error) {
118
+ return { kind: "invalid", error };
105
119
  }
106
120
  };
107
121
  const ensureTrustedFallbackDir = () => {
108
122
  const fallbackPath = fallback();
109
- const state = resolveDirState(fallbackPath);
110
- if (state === "available") {
123
+ const entryPath = directoryEntryPath(fallbackPath, windows ? "win32" : platform);
124
+ let state = resolveDirState(entryPath);
125
+ let finalize = false;
126
+ if (state.kind === "available")
111
127
  return fallbackPath;
112
- }
113
- if (state === "invalid") {
114
- if (tryRepairWritableBits(fallbackPath)) {
115
- return fallbackPath;
128
+ if (state.kind === "missing") {
129
+ try {
130
+ if (!mkdirSync)
131
+ throw new Error("Secure temp creation requires an explicit filesystem adapter.");
132
+ const created = mkdirSync(entryPath, { recursive: true, mode: 0o700 });
133
+ finalize = !windows && (suppliedMkdir !== undefined || created !== undefined);
116
134
  }
117
- throw new Error(`Unsafe fallback ${unsafeFallbackLabel}: ${fallbackPath}`);
118
- }
119
- try {
120
- mkdirSync(fallbackPath, { recursive: true, mode: 0o700 });
121
- chmodSync(fallbackPath, 0o700);
122
- }
123
- catch {
124
- throw new Error(`Unable to create fallback ${unsafeFallbackLabel}: ${fallbackPath}`);
135
+ catch (cause) {
136
+ throw new Error(`Unable to create fallback ${unsafeFallbackLabel}: ${fallbackPath}`, { cause });
137
+ }
138
+ // A concurrent mkdir winner has no implicit ownership or mode authority.
139
+ state = resolveDirState(entryPath);
140
+ if (state.kind === "available" && !finalize)
141
+ return fallbackPath;
125
142
  }
126
- if (resolveDirState(fallbackPath) !== "available" && !tryRepairWritableBits(fallbackPath)) {
127
- throw new Error(`Unsafe fallback ${unsafeFallbackLabel}: ${fallbackPath}`);
143
+ state = tryRepair(entryPath, state, finalize);
144
+ if (state.kind !== "available") {
145
+ throw new Error(`Unsafe fallback ${unsafeFallbackLabel}: ${fallbackPath}`, { cause: state.error });
128
146
  }
129
147
  return fallbackPath;
130
148
  };
131
- if (options.skipPreferredOnWindows === true && platform === "win32") {
149
+ if (skipPreferredOnWindows === true && platform === "win32") {
132
150
  return ensureTrustedFallbackDir();
133
151
  }
134
- if (!options.preferredDir) {
152
+ if (!preferredDir) {
135
153
  return ensureTrustedFallbackDir();
136
154
  }
137
- const existingPreferredState = resolveDirState(options.preferredDir);
138
- if (existingPreferredState === "available") {
139
- return options.preferredDir;
155
+ const entryPath = directoryEntryPath(preferredDir, windows ? "win32" : platform);
156
+ const existingPreferredState = resolveDirState(entryPath);
157
+ if (existingPreferredState.kind === "available") {
158
+ return preferredDir;
140
159
  }
141
- if (existingPreferredState === "invalid") {
142
- if (tryRepairWritableBits(options.preferredDir)) {
143
- return options.preferredDir;
160
+ if (existingPreferredState.kind === "invalid") {
161
+ if (tryRepair(entryPath, existingPreferredState).kind === "available") {
162
+ return preferredDir;
144
163
  }
145
164
  return ensureTrustedFallbackDir();
146
165
  }
166
+ let preferredAvailable = false;
147
167
  try {
148
- const preferredParentDir = path.dirname(options.preferredDir);
168
+ const preferredParentDir = (windows ? path.win32 : path).dirname(entryPath);
149
169
  accessSync(preferredParentDir, TMP_DIR_ACCESS_MODE);
150
- mkdirSync(options.preferredDir, { recursive: true, mode: 0o700 });
151
- chmodSync(options.preferredDir, 0o700);
152
- if (resolveDirState(options.preferredDir) !== "available" &&
153
- !tryRepairWritableBits(options.preferredDir)) {
154
- return ensureTrustedFallbackDir();
170
+ if (mkdirSync) {
171
+ const created = mkdirSync(entryPath, { recursive: true, mode: 0o700 });
172
+ const finalize = !windows && (suppliedMkdir !== undefined || created !== undefined);
173
+ const state = resolveDirState(entryPath);
174
+ preferredAvailable = (!finalize && state.kind === "available") || tryRepair(entryPath, state, finalize).kind === "available";
155
175
  }
156
- return options.preferredDir;
157
176
  }
158
177
  catch {
159
- return ensureTrustedFallbackDir();
178
+ // A failed preferred path never relaxes fallback admission.
160
179
  }
180
+ return preferredAvailable ? preferredDir : ensureTrustedFallbackDir();
161
181
  }
@@ -0,0 +1,35 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ type ExactDirectoryStat = Pick<BigIntStats, "dev" | "ino" | "uid" | "mode" | "isDirectory" | "isSymbolicLink">;
3
+ /** Complete, synchronous descriptor authority for secure-temp admission and repair. */
4
+ export type SecureTempRootDescriptorAdapter = {
5
+ lstatSync(path: string, options: {
6
+ bigint: true;
7
+ }): ExactDirectoryStat;
8
+ fstatSync(fd: number, options: {
9
+ bigint: true;
10
+ }): ExactDirectoryStat;
11
+ openSync(path: string, flags: number): number;
12
+ fchmodSync(fd: number, mode: number): void;
13
+ closeSync(fd: number): void;
14
+ constants: {
15
+ O_RDONLY: number;
16
+ O_DIRECTORY: number;
17
+ O_NOFOLLOW: number;
18
+ O_NONBLOCK: number;
19
+ };
20
+ };
21
+ export type SecureTempDirectoryReceipt = Readonly<{
22
+ dev: bigint;
23
+ ino: bigint;
24
+ uid: bigint;
25
+ mode: bigint;
26
+ }>;
27
+ export type SecureTempRepairAdapter = Omit<SecureTempRootDescriptorAdapter, "constants"> & {
28
+ flags: number;
29
+ };
30
+ export declare function captureSecureTempRepairAdapter(supplied: SecureTempRootDescriptorAdapter | undefined, hasLegacyFsHooks: boolean): SecureTempRepairAdapter | undefined;
31
+ export declare function secureTempDirectoryReceipt(stat: ExactDirectoryStat, uid: number): SecureTempDirectoryReceipt;
32
+ /** No pathname chmod, search-only descriptors, procfs, or reopening fallback. */
33
+ export declare function repairSecureTempDirectory(candidate: string, expected: SecureTempDirectoryReceipt, uid: number, adapter: SecureTempRepairAdapter, access: () => void, repaired: () => void, finalize?: boolean): void;
34
+ export {};
35
+ //# sourceMappingURL=secure-temp-repair.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secure-temp-repair.d.ts","sourceRoot":"","sources":["../src/secure-temp-repair.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAE/C,KAAK,kBAAkB,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,aAAa,GAAG,gBAAgB,CAAC,CAAC;AAE/G,uFAAuF;AACvF,MAAM,MAAM,+BAA+B,GAAG;IAC5C,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,IAAI,CAAA;KAAE,GAAG,kBAAkB,CAAC;IACvE,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,IAAI,CAAA;KAAE,GAAG,kBAAkB,CAAC;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9C,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9F,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAC3G,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,+BAA+B,EAAE,WAAW,CAAC,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7G,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,+BAA+B,GAAG,SAAS,EACrD,gBAAgB,EAAE,OAAO,GACxB,uBAAuB,GAAG,SAAS,CAarC;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,GAAG,0BAA0B,CAS5G;AAgBD,iFAAiF;AACjF,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,0BAA0B,EACpC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,uBAAuB,EAChC,MAAM,EAAE,MAAM,IAAI,EAClB,QAAQ,EAAE,MAAM,IAAI,EACpB,QAAQ,UAAQ,GACf,IAAI,CAuDN"}