@openclaw/fs-safe 0.8.3 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/absolute-path.d.ts.map +1 -1
  3. package/dist/absolute-path.js +8 -7
  4. package/dist/archive-native.d.ts +1 -0
  5. package/dist/archive-native.d.ts.map +1 -1
  6. package/dist/archive-native.js +15 -68
  7. package/dist/archive-plan.d.ts +20 -0
  8. package/dist/archive-plan.d.ts.map +1 -0
  9. package/dist/archive-plan.js +55 -0
  10. package/dist/archive-tar-inspect.d.ts +7 -0
  11. package/dist/archive-tar-inspect.d.ts.map +1 -0
  12. package/dist/archive-tar-inspect.js +59 -0
  13. package/dist/archive-tar.d.ts +3 -1
  14. package/dist/archive-tar.d.ts.map +1 -1
  15. package/dist/archive-tar.js +12 -49
  16. package/dist/archive.d.ts +1 -0
  17. package/dist/archive.d.ts.map +1 -1
  18. package/dist/archive.js +20 -36
  19. package/dist/bounded-read.d.ts.map +1 -1
  20. package/dist/bounded-read.js +31 -4
  21. package/dist/directory-durability.js +5 -5
  22. package/dist/directory-guard.d.ts.map +1 -1
  23. package/dist/directory-guard.js +5 -6
  24. package/dist/file-store-boundary.js +1 -1
  25. package/dist/file-store-prune.js +17 -8
  26. package/dist/file-store.js +2 -2
  27. package/dist/guarded-mkdir.d.ts.map +1 -1
  28. package/dist/guarded-mkdir.js +5 -4
  29. package/dist/native-binding.d.ts +2 -1
  30. package/dist/native-binding.d.ts.map +1 -1
  31. package/dist/native-pinned-write-windows.js +6 -3
  32. package/dist/native-pinned-write.js +3 -3
  33. package/dist/native-staged-file.d.ts +2 -2
  34. package/dist/native-staged-file.d.ts.map +1 -1
  35. package/dist/native-staged-file.js +12 -8
  36. package/dist/opened-realpath.d.ts.map +1 -1
  37. package/dist/opened-realpath.js +10 -9
  38. package/dist/path-policy.js +2 -2
  39. package/dist/pinned-write.d.ts +1 -0
  40. package/dist/pinned-write.d.ts.map +1 -1
  41. package/dist/pinned-write.js +15 -11
  42. package/dist/regular-file.js +8 -8
  43. package/dist/replace-file-descriptor.d.ts.map +1 -1
  44. package/dist/replace-file-descriptor.js +8 -4
  45. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  46. package/dist/replace-file-temp-owner.js +13 -7
  47. package/dist/root-context.js +5 -5
  48. package/dist/root-impl.d.ts +2 -1
  49. package/dist/root-impl.d.ts.map +1 -1
  50. package/dist/root-impl.js +35 -26
  51. package/dist/root-path-existing.d.ts.map +1 -1
  52. package/dist/root-path-existing.js +2 -3
  53. package/dist/root-path-symlink.d.ts.map +1 -1
  54. package/dist/root-path-symlink.js +2 -3
  55. package/dist/root-path.d.ts.map +1 -1
  56. package/dist/root-path.js +3 -4
  57. package/dist/root-paths.d.ts.map +1 -1
  58. package/dist/root-paths.js +16 -15
  59. package/dist/root-write-mode.d.ts.map +1 -1
  60. package/dist/root-write-mode.js +5 -4
  61. package/dist/root-write-verification.js +6 -6
  62. package/dist/secret-file.js +2 -2
  63. package/dist/strict-file-identity.d.ts +1 -1
  64. package/dist/strict-file-identity.d.ts.map +1 -1
  65. package/dist/symlink-parents.d.ts.map +1 -1
  66. package/dist/symlink-parents.js +1 -2
  67. package/docs/archive.md +57 -0
  68. package/docs/reading.md +2 -0
  69. package/docs/root.md +10 -1
  70. package/docs/types.md +2 -1
  71. package/docs/writing.md +26 -3
  72. package/package.json +8 -8
@@ -1 +1 @@
1
- {"version":3,"file":"root-path.d.ts","sourceRoot":"","sources":["../src/root-path.ts"],"names":[],"mappings":"AAkBA,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,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,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,CAM3B;AAsCD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,qBAAqB,GAAG,gBAAgB,CAMnF"}
1
+ {"version":3,"file":"root-path.d.ts","sourceRoot":"","sources":["../src/root-path.ts"],"names":[],"mappings":"AAiBA,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,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,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,CAM3B;AAsCD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,qBAAqB,GAAG,gBAAgB,CAMnF"}
package/dist/root-path.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import fs from "node:fs";
2
- import fsp from "node:fs/promises";
3
2
  import path from "node:path";
4
3
  import { formatErrorDetail, shortPath } from "./error-detail.js";
5
4
  import { FsSafeError } from "./errors.js";
@@ -234,7 +233,7 @@ async function resolveRootPathLexicalAsync(params) {
234
233
  state.lexicalCursor = path.join(state.lexicalCursor, segment);
235
234
  let stat;
236
235
  try {
237
- stat = await fsp.lstat(state.lexicalCursor);
236
+ stat = fs.lstatSync(state.lexicalCursor);
238
237
  }
239
238
  catch (error) {
240
239
  if (handleLexicalLstatFailure(context, error, idx))
@@ -423,8 +422,8 @@ function buildResolvedRootPath(params) {
423
422
  async function getPathKind(absolutePath, preserveFinalSymlink) {
424
423
  try {
425
424
  const stat = preserveFinalSymlink
426
- ? await fsp.lstat(absolutePath)
427
- : await fsp.stat(absolutePath);
425
+ ? fs.lstatSync(absolutePath)
426
+ : fs.statSync(absolutePath);
428
427
  return { exists: true, kind: toResolvedKind(stat) };
429
428
  }
430
429
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"root-paths.d.ts","sourceRoot":"","sources":["../src/root-paths.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"root-paths.d.ts","sourceRoot":"","sources":["../src/root-paths.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import fsSync from "node:fs";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { formatErrorDetail } from "./error-detail.js";
@@ -16,7 +17,7 @@ function pathStaysWithinRoot(rootDir, candidatePath) {
16
17
  }
17
18
  async function resolveRealPathIfExists(targetPath) {
18
19
  try {
19
- return await fs.realpath(targetPath);
20
+ return fsSync.realpathSync.native(targetPath);
20
21
  }
21
22
  catch {
22
23
  return undefined;
@@ -24,11 +25,11 @@ async function resolveRealPathIfExists(targetPath) {
24
25
  }
25
26
  async function resolveTrustedRootRealPath(rootDir) {
26
27
  try {
27
- const rootLstat = await fs.lstat(rootDir);
28
+ const rootLstat = fsSync.lstatSync(rootDir);
28
29
  if (!rootLstat.isDirectory() || rootLstat.isSymbolicLink()) {
29
30
  return undefined;
30
31
  }
31
- return await fs.realpath(rootDir);
32
+ return fsSync.realpathSync.native(rootDir);
32
33
  }
33
34
  catch {
34
35
  return undefined;
@@ -36,7 +37,7 @@ async function resolveTrustedRootRealPath(rootDir) {
36
37
  }
37
38
  async function validateCanonicalPathWithinRoot(params) {
38
39
  try {
39
- const candidateLstat = await fs.lstat(params.candidatePath);
40
+ const candidateLstat = fsSync.lstatSync(params.candidatePath);
40
41
  if (candidateLstat.isSymbolicLink()) {
41
42
  return "invalid";
42
43
  }
@@ -49,7 +50,7 @@ async function validateCanonicalPathWithinRoot(params) {
49
50
  if (params.expect === "file" && candidateLstat.nlink > 1) {
50
51
  return "invalid";
51
52
  }
52
- const candidateRealPath = await fs.realpath(params.candidatePath);
53
+ const candidateRealPath = fsSync.realpathSync.native(params.candidatePath);
53
54
  return isPathInside(params.rootRealPath, candidateRealPath) ? "ok" : "invalid";
54
55
  }
55
56
  catch (err) {
@@ -109,7 +110,7 @@ async function resolveNearestExistingPath(targetPath) {
109
110
  let current = path.resolve(targetPath);
110
111
  while (true) {
111
112
  try {
112
- await fs.lstat(current);
113
+ fsSync.lstatSync(current);
113
114
  return current;
114
115
  }
115
116
  catch (err) {
@@ -133,7 +134,7 @@ async function assertNoSymlinkSegments(params) {
133
134
  for (const segment of relative.split(path.sep).filter(Boolean)) {
134
135
  current = path.join(current, segment);
135
136
  try {
136
- const stat = await fs.lstat(current);
137
+ const stat = fsSync.lstatSync(current);
137
138
  if (stat.isSymbolicLink()) {
138
139
  throw new FsSafeError("symlink", `Invalid path: must not traverse symlinks within ${params.scopeLabel}`);
139
140
  }
@@ -163,14 +164,14 @@ export async function ensureDirectoryWithinRoot(params) {
163
164
  return lexical;
164
165
  const rootDir = path.resolve(params.rootDir);
165
166
  const targetPath = lexical.path;
166
- const rootStat = await fs.lstat(rootDir);
167
+ const rootStat = fsSync.lstatSync(rootDir);
167
168
  if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
168
169
  return invalidPath(scopeLabel);
169
170
  }
170
171
  await assertNoSymlinkSegments({ rootDir, targetPath, scopeLabel });
171
- const rootReal = await fs.realpath(rootDir);
172
+ const rootReal = fsSync.realpathSync.native(rootDir);
172
173
  const nearestExistingPath = await resolveNearestExistingPath(targetPath);
173
- const nearestExistingReal = await fs.realpath(nearestExistingPath);
174
+ const nearestExistingReal = fsSync.realpathSync.native(nearestExistingPath);
174
175
  if (!isPathInside(rootReal, nearestExistingReal)) {
175
176
  return invalidPath(scopeLabel);
176
177
  }
@@ -180,7 +181,7 @@ export async function ensureDirectoryWithinRoot(params) {
180
181
  current = path.join(current, segment);
181
182
  while (true) {
182
183
  try {
183
- const stat = await fs.lstat(current);
184
+ const stat = fsSync.lstatSync(current);
184
185
  if (stat.isSymbolicLink() || !stat.isDirectory()) {
185
186
  return invalidPath(scopeLabel);
186
187
  }
@@ -201,12 +202,12 @@ export async function ensureDirectoryWithinRoot(params) {
201
202
  }
202
203
  }
203
204
  }
204
- const currentReal = await fs.realpath(current);
205
+ const currentReal = fsSync.realpathSync.native(current);
205
206
  if (!isPathInside(rootReal, currentReal)) {
206
207
  return invalidPath(scopeLabel);
207
208
  }
208
209
  }
209
- const targetReal = await fs.realpath(targetPath);
210
+ const targetReal = fsSync.realpathSync.native(targetPath);
210
211
  if (!isPathInside(rootReal, targetReal)) {
211
212
  return invalidPath(scopeLabel);
212
213
  }
@@ -308,7 +309,7 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
308
309
  return lexicalPathResult;
309
310
  }
310
311
  try {
311
- const resolvedExistingPath = await fs.realpath(raw);
312
+ const resolvedExistingPath = fsSync.realpathSync.native(raw);
312
313
  const relativePath = path.relative(rootRealPath, resolvedExistingPath);
313
314
  if (!isInRoot(relativePath)) {
314
315
  return lexicalPathResult;
@@ -350,7 +351,7 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
350
351
  scopeLabel: params.scopeLabel,
351
352
  });
352
353
  const existingPath = await resolveNearestExistingPath(pathResult.fallbackPath);
353
- const existingRealPath = await fs.realpath(existingPath);
354
+ const existingRealPath = fsSync.realpathSync.native(existingPath);
354
355
  if (!isPathInside(rootRealPath, existingRealPath)) {
355
356
  return invalidPath(params.scopeLabel);
356
357
  }
@@ -1 +1 @@
1
- {"version":3,"file":"root-write-mode.d.ts","sourceRoot":"","sources":["../src/root-write-mode.ts"],"names":[],"mappings":"AASA,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAwClB"}
1
+ {"version":3,"file":"root-write-mode.d.ts","sourceRoot":"","sources":["../src/root-write-mode.ts"],"names":[],"mappings":"AAUA,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAwClB"}
@@ -1,3 +1,4 @@
1
+ import fsSync from "node:fs";
1
2
  import fs from "node:fs/promises";
2
3
  import { FsSafeError } from "./errors.js";
3
4
  import { sameFileIdentity } from "./file-identity.js";
@@ -8,7 +9,7 @@ import { inspectFileIdentity } from "./strict-file-identity.js";
8
9
  // The caller has already resolved and guarded this write target.
9
10
  export async function inheritWriteTargetMode(params) {
10
11
  try {
11
- const existing = await inspectFileIdentity(() => fs.lstat(params.targetPath, { bigint: true }));
12
+ const existing = await inspectFileIdentity(() => fsSync.lstatSync(params.targetPath, { bigint: true }));
12
13
  if (existing.isSymbolicLink())
13
14
  throw new FsSafeError("path-alias", "path alias escape blocked");
14
15
  if (!existing.isFile())
@@ -22,7 +23,7 @@ export async function inheritWriteTargetMode(params) {
22
23
  const handle = await fs.open(params.targetPath, resolveReadOpenFlags());
23
24
  try {
24
25
  // Bind admission to the inode whose metadata is inherited.
25
- if (!sameFileIdentity(await handle.stat({ bigint: true }), existing)) {
26
+ if (!sameFileIdentity(fsSync.fstatSync(handle.fd, { bigint: true }), existing)) {
26
27
  throw new FsSafeError("path-mismatch", "write target changed during mode inheritance");
27
28
  }
28
29
  }
@@ -32,11 +33,11 @@ export async function inheritWriteTargetMode(params) {
32
33
  try {
33
34
  // A parent can change after guarded resolution. Do not inherit metadata
34
35
  // from an outside inode, even if the parent is restored before publication.
35
- const realPath = await fs.realpath(params.targetPath);
36
+ const realPath = fsSync.realpathSync.native(params.targetPath);
36
37
  if (!isPathInside(params.rootWithSep, realPath))
37
38
  throw outsideWorkspaceError();
38
39
  await inspectFileIdentity(async () => {
39
- const current = await fs.stat(realPath, { bigint: true });
40
+ const current = fsSync.statSync(realPath, { bigint: true });
40
41
  if (!current.isFile())
41
42
  throw new FsSafeError("not-file", "not a file");
42
43
  if (current.nlink > 1n)
@@ -46,16 +46,16 @@ export async function verifyAtomicWriteResult(params) {
46
46
  try {
47
47
  // This descriptor remains owned by the writer, even when final mode forbids opens.
48
48
  const stat = assertDescriptor();
49
- assertPath(await fs.lstat(params.targetPath, { bigint: true }));
49
+ assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
50
50
  const { realPath } = await resolveOpenedFileRealPathForFd(params.fd, stat, params.targetPath);
51
- assertPath(await fs.stat(realPath, { bigint: true }));
51
+ assertPath(fsSync.statSync(realPath, { bigint: true }));
52
52
  if (!isPathInside(params.root.rootWithSep, realPath)) {
53
53
  throw outsideWorkspaceError();
54
54
  }
55
55
  await assertAsyncDirectoryGuard(params.parentGuard);
56
56
  await assertRootIdentityCurrent(params.root);
57
57
  // Recheck after canonical resolution and directory checks, including late links.
58
- assertPath(await fs.lstat(params.targetPath, { bigint: true }));
58
+ assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
59
59
  assertDescriptor();
60
60
  if (needsPathOpen) {
61
61
  // A retained fd cannot prove that an opaque Windows pathname still names it.
@@ -73,15 +73,15 @@ export async function verifyAtomicWriteResult(params) {
73
73
  });
74
74
  try {
75
75
  const reopenedStat = assertDescriptor(opened.fd);
76
- assertPath(await fs.lstat(params.targetPath, { bigint: true }));
76
+ assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
77
77
  const { realPath: reopenedPath } = await resolveOpenedFileRealPathForFd(opened.fd, reopenedStat, params.targetPath);
78
- assertPath(await fs.stat(reopenedPath, { bigint: true }));
78
+ assertPath(fsSync.statSync(reopenedPath, { bigint: true }));
79
79
  if (!isPathInside(params.root.rootWithSep, reopenedPath)) {
80
80
  throw outsideWorkspaceError();
81
81
  }
82
82
  await assertAsyncDirectoryGuard(params.parentGuard);
83
83
  await assertRootIdentityCurrent(params.root);
84
- assertPath(await fs.lstat(params.targetPath, { bigint: true }));
84
+ assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
85
85
  assertDescriptor(opened.fd);
86
86
  }
87
87
  finally {
@@ -140,7 +140,7 @@ async function enforcePrivateDirectoryMode(params) {
140
140
  }
141
141
  async function inspectPrivateDirectory(directory, kind) {
142
142
  return await inspectFileIdentity(async () => {
143
- const stat = await fsp.lstat(directory, { bigint: true });
143
+ const stat = fs.lstatSync(directory, { bigint: true });
144
144
  if (stat.isSymbolicLink()) {
145
145
  throw new Error(`Private secret ${kind} ${directory} must not be a symlink.`);
146
146
  }
@@ -260,7 +260,7 @@ export async function prepareSecretFileWrite(params) {
260
260
  async function materializeSecretFileAtomic(params, createOnly) {
261
261
  const { mode, rootGuard, parentGuard, fileName, finalFilePath } = await prepareSecretFileWrite(params);
262
262
  try {
263
- const stat = await fsp.lstat(finalFilePath);
263
+ const stat = fs.lstatSync(finalFilePath);
264
264
  if (createOnly) {
265
265
  throw new FsSafeError("secret-exists", `Private secret file ${finalFilePath} already exists.`);
266
266
  }
@@ -1,6 +1,6 @@
1
1
  import type { BigIntStats } from "node:fs";
2
2
  type ExactFileIdentity = Pick<BigIntStats, "dev" | "ino">;
3
- export declare function inspectFileIdentity<T extends ExactFileIdentity>(inspect: () => Promise<T>, expected?: ExactFileIdentity, platform?: NodeJS.Platform): Promise<T>;
3
+ export declare function inspectFileIdentity<T extends ExactFileIdentity>(inspect: () => T | Promise<T>, expected?: ExactFileIdentity, platform?: NodeJS.Platform): Promise<T>;
4
4
  export declare function inspectFileIdentitySync<T extends ExactFileIdentity>(inspect: () => T, expected?: ExactFileIdentity, platform?: NodeJS.Platform): T;
5
5
  export {};
6
6
  //# sourceMappingURL=strict-file-identity.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"strict-file-identity.d.ts","sourceRoot":"","sources":["../src/strict-file-identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAI3C,KAAK,iBAAiB,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC;AA+B1D,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,iBAAiB,EACnE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,iBAAiB,EACjE,OAAO,EAAE,MAAM,CAAC,EAChB,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,CAAC,CAOH"}
1
+ {"version":3,"file":"strict-file-identity.d.ts","sourceRoot":"","sources":["../src/strict-file-identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAI3C,KAAK,iBAAiB,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC;AA+B1D,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,iBAAiB,EACnE,OAAO,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAC7B,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,iBAAiB,EACjE,OAAO,EAAE,MAAM,CAAC,EAChB,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,CAAC,CAOH"}
@@ -1 +1 @@
1
- {"version":3,"file":"symlink-parents.d.ts","sourceRoot":"","sources":["../src/symlink-parents.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAyBF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,6BAA6B,GACpC,IAAI,CA6BN"}
1
+ {"version":3,"file":"symlink-parents.d.ts","sourceRoot":"","sources":["../src/symlink-parents.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAyBF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,6BAA6B,GACpC,IAAI,CA6BN"}
@@ -1,5 +1,4 @@
1
1
  import fsSync from "node:fs";
2
- import fs from "node:fs/promises";
3
2
  import path from "node:path";
4
3
  import { FsSafeError } from "./errors.js";
5
4
  import { hasNodeErrorCode, isPathRelativeEscape } from "./path.js";
@@ -30,7 +29,7 @@ export async function assertNoSymlinkParents(params) {
30
29
  for (const [index, segment] of walk.segments.entries()) {
31
30
  current = path.join(current, segment);
32
31
  try {
33
- const stat = await fs.lstat(current);
32
+ const stat = fsSync.lstatSync(current);
34
33
  if (stat.isSymbolicLink()) {
35
34
  if (params.allowRootChildSymlink && path.dirname(current) === walk.root) {
36
35
  continue;
package/docs/archive.md CHANGED
@@ -422,6 +422,63 @@ Normal link/filter policy still governs the described member. Canonical
422
422
  pre-strip filter paths, decoded-stream ceilings, and physical EOF checks apply
423
423
  to plain/gzip TAR and native zstd/bzip2 alike.
424
424
 
425
+ ## `inspectTarArchive`
426
+
427
+ Inspect accepted TAR members without creating an extracted tree. This operation
428
+ uses the same complete Rust/WASM admission and TypeScript extraction planner as
429
+ `extractArchive`, with zero stripping. It detects plain TAR or gzip from the
430
+ input bytes; ZIP, zstd, and bzip2 are not part of this inspection API.
431
+
432
+ ```ts
433
+ import { inspectTarArchive } from "@openclaw/fs-safe/archive";
434
+
435
+ const entries = await inspectTarArchive({
436
+ archivePath: "/srv/uploads/tree.tar.gz",
437
+ timeoutMs: 30_000,
438
+ limits: {
439
+ maxArchiveBytes: 16 * 1024 * 1024,
440
+ maxEntries: 5_000,
441
+ maxEntryBytes: 16 * 1024 * 1024,
442
+ maxExtractedBytes: 64 * 1024 * 1024,
443
+ },
444
+ entryFilter: ({ kind }) => kind === "file" || kind === "directory" ? "extract" : "skip",
445
+ onFiltered: "reject-archive",
446
+ });
447
+ ```
448
+
449
+ `InspectTarArchiveOptions` accepts `archivePath`, `timeoutMs`, `limits`,
450
+ `entryFilter`, and `onFiltered`, with the same defaults and error classes as
451
+ extraction. The result is a frozen array of frozen `InspectedTarEntry` records:
452
+ `{ path: string; kind: "file" | "directory"; size: number }`, in archive order.
453
+ `size` is the effective declared payload length. `path` is extraction's
454
+ canonical pre-strip identity: case, Unicode spelling, BOM, and embedded LF are
455
+ preserved, while separators and dot components follow the existing archive path
456
+ contract. No human-readable tar listing or escape decoding is involved.
457
+
458
+ Full framing, gzip integrity, EOF, metadata, decoded-byte, and manifest-budget
459
+ validation finishes before the caller's filter runs. The shared planner then
460
+ applies traversal, collision, depth, blocked-type, and accepted-payload limits.
461
+ A failure returns no partial result. Filter callbacks are decisions, not admission
462
+ receipts: later policy, collision, or budget checks can still reject the archive.
463
+ Only the resolved Promise/result is authorization-worthy; do not perform
464
+ irreversible actions from a filter callback.
465
+
466
+ Root-only records count toward entry limits but produce no result; PAX/GNU metadata headers are not members. Only accepted
467
+ file/directory members appear, not implicit parent directories. Unsupported
468
+ records follow extraction's omission policy unless the filter rejects them, as
469
+ in the example. AppleDouble records encoded as ordinary files are ordinary
470
+ members, not hidden metadata.
471
+
472
+ Inspection pins and privately stages its input, then cleans up that copy. It
473
+ neither creates destination paths nor tests destination permissions or platform
474
+ filename restrictions. Its result is evidence about those inspected bytes, not
475
+ an extraction capability or a promise that a later file at `archivePath` is
476
+ unchanged. Callers making authorization decisions must retain the same private
477
+ immutable archive or verify byte identity before extracting with matching
478
+ filter/limit settings. Extraction always performs its own admission and guarded
479
+ publication. Native `off`, `auto`, and `require` retain their existing selection
480
+ and availability semantics; inspection does not fall back after native failure.
481
+
425
482
  ## `resolveArchiveKind`
426
483
 
427
484
  ```ts
package/docs/reading.md CHANGED
@@ -12,6 +12,8 @@ const opened = await fs.open("large.log"); // FileHandle for strea
12
12
 
13
13
  ## What every read does
14
14
 
15
+ Identity and containment checks use brief synchronous metadata calls, like Node's module resolution, while file data is read asynchronously. Canonical paths retain Node's native realpath spelling, including expansion of Windows short paths.
16
+
15
17
  Regardless of shape, every read goes through the same boundary checks:
16
18
 
17
19
  1. Resolve the input lexically against the canonical real root.
package/docs/root.md CHANGED
@@ -18,6 +18,7 @@ const fs = await root("/srv/workspace", {
18
18
  function root(rootDir: string, defaults?: RootDefaults): Promise<Root>;
19
19
 
20
20
  type RootDefaults = {
21
+ durable?: boolean; // fsync write/create/writeJson/createJson/append; default true
21
22
  hardlinks?: "reject" | "allow"; // refuse files with nlink > 1 on read; defaults to "reject"
22
23
  denyMutations?: DenyMutationPolicy; // absolute paths/prefixes mutation methods may not change
23
24
  maxBytes?: number; // refuse reads larger than this many bytes; defaults to 16 MiB
@@ -99,7 +100,7 @@ fs.write(rel, data, options?) // overwrite-ok atomic write
99
100
  fs.create(rel, data, options?) // throws "already-exists" if target exists
100
101
  fs.writeJson(rel, value, options?) // JSON.stringify + atomic write
101
102
  fs.createJson(rel, value, options?) // create() variant of writeJson
102
- fs.append(rel, data, options?) // append text/buffer; syncs before close
103
+ fs.append(rel, data, options?) // append text/buffer; syncs before close by default
103
104
  fs.copyIn(rel, sourceAbsPath, options?) // copy from outside the root, atomically, with size cap
104
105
  fs.openWritable(rel, options?) // FileHandle for streaming writes; supports await using
105
106
  fs.move(from, to, options?) // rename within the root; defaults to no clobber
@@ -110,6 +111,14 @@ fs.ensureRoot(options?) // accepts "" / "." as the root itself
110
111
 
111
112
  `write`, `create`, `append`, `writeJson`, and `createJson` accept `mode?: number`; use `0o600` for credentials and other private state. `writeJson` also accepts the same options as `JSON.stringify` plus `trailingNewline?: boolean` (defaults `true` so the file ends in `\n`).
112
113
 
114
+ These five methods also accept `durable?: boolean`: the per-call value overrides
115
+ `Root.defaults.durable`, which defaults to `true` when omitted. An explicitly
116
+ `undefined` per-call value preserves the root default. `durable: false` keeps
117
+ the existing publication behavior, modes, and identity checks but skips file
118
+ and parent-directory fsync calls. Use it only for reconstructible data: a crash
119
+ may lose the write or leave the previous file. See [Writing](writing.md#write-options)
120
+ for platform details.
121
+
113
122
  `copyIn` is a one-shot ingest from a trusted absolute source path: it streams the source through the boundary, atomically renames into the root, and respects `maxBytes`.
114
123
 
115
124
  Root operations that choose a new destination reject a leading Windows
package/docs/types.md CHANGED
@@ -100,6 +100,7 @@ type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
100
100
 
101
101
  type RootDefaults = {
102
102
  denyMutations?: DenyMutationPolicy;
103
+ durable?: boolean; // default true for write/create/writeJson/createJson/append
103
104
  hardlinks?: "reject" | "allow";
104
105
  maxBytes?: number;
105
106
  mkdir?: boolean; // default true for mutation methods
@@ -126,7 +127,7 @@ type RootOptions = {
126
127
 
127
128
  ```ts
128
129
  type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "nonBlockingRead" | "symlinks">;
129
- type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "mkdir" | "mode" | "renameIdentity"> & {
130
+ type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "durable" | "mkdir" | "mode" | "renameIdentity"> & {
130
131
  encoding?: BufferEncoding;
131
132
  overwrite?: boolean;
132
133
  };
package/docs/writing.md CHANGED
@@ -77,14 +77,37 @@ await fs.write("state/last-run.json", JSON.stringify(run));
77
77
  await fs.write("notes/today.txt", "hello\n", { encoding: "utf8" });
78
78
  ```
79
79
 
80
- `data` accepts `string | Buffer`. `options` are `{ denyMutations?: DenyMutationPolicy; encoding?: BufferEncoding; mkdir?: boolean; mode?: number; overwrite?: boolean; renameIdentity?: RenameIdentityPolicy }`. `mode` sets the file's POSIX mode. If neither the call nor `RootDefaults` supplies it, a replacement preserves the existing file mode and a new file uses `0o600`. `mkdir` and `overwrite` both default to `true`; set `overwrite: false` for the same no-clobber behavior as `create()`.
80
+ `data` accepts `string | Buffer`. `mode` sets the file's POSIX mode. If neither the call nor `RootDefaults` supplies it, a replacement preserves the existing file mode and a new file uses `0o600`. `mkdir` and `overwrite` both default to `true`; set `overwrite: false` for the same no-clobber behavior as `create()`.
81
+
82
+ #### Write options
83
+
84
+ | Option | Type | Default / behavior |
85
+ |---|---|---|
86
+ | `denyMutations` | `DenyMutationPolicy` | Merged with root-level denies. |
87
+ | `durable` | `boolean` | `true`; use `false` to skip file and parent fsync. |
88
+ | `encoding` | `BufferEncoding` | `"utf8"` for strings. |
89
+ | `mkdir` | `boolean` | `true`; creates missing parents. |
90
+ | `mode` | `number` | Inherited on replacement, otherwise `0o600`. |
91
+ | `overwrite` | `boolean` | `true`; `false` is create-only. |
92
+ | `renameIdentity` | `RenameIdentityPolicy` | `"strict"`. |
93
+
94
+ `write`, `create`, `writeJson`, `createJson`, and `append` accept `durable`.
95
+ Precedence is per-call option, then `Root.defaults.durable`, then `true`;
96
+ an explicitly `undefined` call option preserves the root default.
97
+ `durable: false` keeps the sibling-temp replace/rename behavior of replacement
98
+ writes but skips file and parent-directory fsync calls. Create-only and append
99
+ publication behavior, permissions, identity checks, and error codes are unchanged.
100
+ Use it only for reconstructible data: a crash may lose the write or leave the
101
+ previous file. `copyIn`, `move`, and streaming `openWritable` do not use this option.
102
+ The existing pure-JavaScript Windows writer performs no fsync calls in either
103
+ setting; native Windows writes honor the option. Directory sync remains best-effort.
81
104
 
82
105
  POSIX modes without read permission, including `0o000` and `0o200`, succeed:
83
106
  final verification uses a descriptor retained by the writer rather than reopening
84
107
  the published file. The requested mode is not relaxed for verification.
85
108
  Publication verification compares exact bigint descriptor and pathname identities,
86
109
  including large file indexes that cannot be represented by a JavaScript number.
87
- Native publication syncs content before rename and always syncs the parent directory.
110
+ With durability enabled (the default), native publication syncs content before rename and syncs the parent directory.
88
111
  Modes that retain owner read/write skip the extra mode-only file sync: after a
89
112
  crash, the file may retain staged mode `0o600` instead of the wider requested mode.
90
113
  Modes that remove owner read or write, and corrections of observed wider staging
@@ -143,7 +166,7 @@ type RootWriteJsonOptions = RootWriteOptions & {
143
166
 
144
167
  ### `fs.append(rel, data, options?)`
145
168
 
146
- Open in append mode, write, sync the file handle, and close. Honors `mkdir` for the parent directory and syncs the parent directory when the append creates the file. Pass `prependNewlineIfNeeded: true` to insert a `\n` if the file does not already end in one.
169
+ Open in append mode, write, sync the file handle, and close. Honors `mkdir` for the parent directory and syncs the parent directory when the append creates the file. `durable: false` skips both syncs. Pass `prependNewlineIfNeeded: true` to insert a `\n` if the file does not already end in one.
147
170
 
148
171
  ```ts
149
172
  await fs.append("logs/today.log", `[${ts}] ${line}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "keywords": [
6
6
  "filesystem",
@@ -156,13 +156,13 @@
156
156
  "archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
157
157
  },
158
158
  "optionalDependencies": {
159
- "@openclaw/fs-safe-darwin-arm64": "0.8.3",
160
- "@openclaw/fs-safe-darwin-x64": "0.8.3",
161
- "@openclaw/fs-safe-linux-arm64-gnu": "0.8.3",
162
- "@openclaw/fs-safe-linux-arm64-musl": "0.8.3",
163
- "@openclaw/fs-safe-linux-x64-gnu": "0.8.3",
164
- "@openclaw/fs-safe-linux-x64-musl": "0.8.3",
165
- "@openclaw/fs-safe-win32-x64-msvc": "0.8.3",
159
+ "@openclaw/fs-safe-darwin-arm64": "0.8.4",
160
+ "@openclaw/fs-safe-darwin-x64": "0.8.4",
161
+ "@openclaw/fs-safe-linux-arm64-gnu": "0.8.4",
162
+ "@openclaw/fs-safe-linux-arm64-musl": "0.8.4",
163
+ "@openclaw/fs-safe-linux-x64-gnu": "0.8.4",
164
+ "@openclaw/fs-safe-linux-x64-musl": "0.8.4",
165
+ "@openclaw/fs-safe-win32-x64-msvc": "0.8.4",
166
166
  "jszip": "^3.10.1"
167
167
  },
168
168
  "devDependencies": {