@openclaw/feishu 2026.9.8 → 2026.10.1-beta.2
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.
- package/dist/.setup/{channel-DV4hM2Uf.mjs → channel-CX62T71S.mjs} +146 -245
- package/dist/.setup/{channel.runtime-CXQcH5zZ.mjs → channel.runtime-COiaq-Rv.mjs} +6 -12
- package/dist/.setup/{chat-D8MlFAZi.mjs → chat-BdyvImsY.mjs} +6 -9
- package/dist/.setup/{client-BYG-_IZl.mjs → client-BsBVWJzN.mjs} +1 -1
- package/dist/.setup/{doctor-contract-3XC0vbIw.mjs → doctor-contract-B1B9ogdd.mjs} +212 -27
- package/dist/.setup/{monitor-D_rsHBZ6.mjs → monitor-7DvwUs4Y.mjs} +5 -6
- package/dist/.setup/{monitor.account-DvvY7i6k.mjs → monitor.account-7xVu7XWY.mjs} +190 -393
- package/dist/.setup/{probe-_ViqSi0j.mjs → probe-B9o7iey6.mjs} +13 -39
- package/dist/.setup/{reply-delivery-result-COAjaukc.mjs → reply-delivery-result-Bt-BWBEA.mjs} +151 -297
- package/dist/.setup/{setup-api-C4S5I3ac.mjs → setup-api-DmzX9S01.mjs} +1 -1
- package/dist/.setup/{subagent-hooks-BaFoMu8W.mjs → subagent-hooks-CUXg0pC-.mjs} +5 -11
- package/dist/.setup/{thread-bindings-EdLXPXzw.mjs → thread-bindings-jW2x5VuN.mjs} +23 -41
- package/dist/api.js +234 -297
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +2 -2
- package/dist/session-binding-contract-api.js +1 -1
- package/dist/setup-api.js +1 -1
- package/dist/setup-entry.js +1 -1
- package/dist/subagent-hooks-api.js +1 -1
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +95 -0
- package/node_modules/@openclaw/fs-safe/README.md +70 -379
- package/node_modules/@openclaw/fs-safe/dist/absolute-path.js +35 -51
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-input.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +9 -10
- package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.d.ts +51 -0
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +242 -0
- package/node_modules/@openclaw/fs-safe/dist/config.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/config.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/create.js +12 -5
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +11 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +4 -7
- package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.d.ts +55 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +262 -0
- package/node_modules/@openclaw/fs-safe/dist/exclusive-create.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/exclusive-create.js +22 -0
- package/node_modules/@openclaw/fs-safe/dist/file-cleanup.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/file-cleanup.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-identity.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-mutation.js +9 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.js +4 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-stale-admission.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -0
- package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/file-store-sync-write.js +20 -18
- package/node_modules/@openclaw/fs-safe/dist/file-store.js +4 -10
- package/node_modules/@openclaw/fs-safe/dist/fs.d.ts +2 -10
- package/node_modules/@openclaw/fs-safe/dist/fs.js +2 -10
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +3 -4
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +6 -18
- package/node_modules/@openclaw/fs-safe/dist/guest-dispatch-python.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +0 -2
- package/node_modules/@openclaw/fs-safe/dist/guest.js +3 -9
- package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-directory.js +1 -5
- package/node_modules/@openclaw/fs-safe/dist/json.js +19 -37
- package/node_modules/@openclaw/fs-safe/dist/move-path.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +47 -2
- package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +1 -9
- package/node_modules/@openclaw/fs-safe/dist/native-config.js +6 -40
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +5 -2
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +11 -316
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.d.ts +7 -7
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.js +28 -175
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +15 -0
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +418 -0
- package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +9 -4
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +17 -13
- package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +6 -24
- package/node_modules/@openclaw/fs-safe/dist/native.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/path-case.js +10 -9
- package/node_modules/@openclaw/fs-safe/dist/path-scope-lexical.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/path-segment-route.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/path-segment-route.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/path.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +1 -3
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +9 -10
- package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +3 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +18 -32
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +5 -17
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +92 -230
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.d.ts +4 -15
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.js +22 -63
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +9 -34
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +78 -83
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +18 -20
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +64 -98
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +16 -33
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +61 -200
- package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/replace-file.js +76 -216
- package/node_modules/@openclaw/fs-safe/dist/root-boundary.js +14 -24
- package/node_modules/@openclaw/fs-safe/dist/root-context.d.ts +1 -4
- package/node_modules/@openclaw/fs-safe/dist/root-context.js +4 -19
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +52 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +350 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +35 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -27
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +31 -28
- package/node_modules/@openclaw/fs-safe/dist/root-directory.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/root-errors.js +34 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file.js +9 -21
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +105 -37
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +7 -2
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +67 -38
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -2
- package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -3
- package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/root-path-stat.js +68 -85
- package/node_modules/@openclaw/fs-safe/dist/root-path.js +6 -1
- package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-paths.js +4 -9
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +311 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove.d.ts +11 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +1 -3
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.d.ts +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -10
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +0 -4
- package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -9
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.d.ts +0 -2
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/sibling-temp.js +7 -4
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +13 -6
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +0 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +5 -23
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +11 -6
- package/node_modules/@openclaw/fs-safe/dist/temp-cleanup.js +1 -8
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +2 -9
- package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/trash.js +6 -2
- package/node_modules/@openclaw/fs-safe/dist/walk.js +100 -46
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +7 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +235 -19
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +21 -2
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +21 -3
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +111 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +27 -4
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/watch.js +131 -40
- package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.js +24 -6
- package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.js +51 -0
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -0
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +4 -1
- package/node_modules/@openclaw/fs-safe/docs/config.md +6 -14
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +4 -14
- package/node_modules/@openclaw/fs-safe/docs/durability.md +6 -29
- package/node_modules/@openclaw/fs-safe/docs/entry-publication.md +146 -0
- package/node_modules/@openclaw/fs-safe/docs/errors.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/file-store.md +28 -5
- package/node_modules/@openclaw/fs-safe/docs/index.md +2 -31
- package/node_modules/@openclaw/fs-safe/docs/install.md +10 -31
- package/node_modules/@openclaw/fs-safe/docs/json.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/local-roots.md +0 -1
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +12 -12
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +35 -96
- package/node_modules/@openclaw/fs-safe/docs/native.md +22 -20
- package/node_modules/@openclaw/fs-safe/docs/path.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/reading.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/root.md +26 -184
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +115 -3
- package/node_modules/@openclaw/fs-safe/docs/store.md +2 -2
- package/node_modules/@openclaw/fs-safe/docs/temp.md +29 -65
- package/node_modules/@openclaw/fs-safe/docs/testing.md +90 -58
- package/node_modules/@openclaw/fs-safe/docs/types.md +3 -16
- package/node_modules/@openclaw/fs-safe/docs/walk.md +33 -5
- package/node_modules/@openclaw/fs-safe/docs/watch.md +96 -14
- package/node_modules/@openclaw/fs-safe/docs/writing.md +172 -16
- package/node_modules/@openclaw/fs-safe/package.json +10 -10
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/package.json +5 -5
- package/skills/feishu-wiki/SKILL.md +1 -1
- package/dist/.setup/accounts-wRqItHug.mjs +0 -206
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +0 -6
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +0 -88
- package/node_modules/@openclaw/fs-safe/docs/mutation-policy-proof.md +0 -69
- package/node_modules/@openclaw/fs-safe/docs/private-file-store.md +0 -68
- package/node_modules/@openclaw/fs-safe/docs/test-hooks.md +0 -110
|
@@ -22,7 +22,7 @@ export function handleSyncStaleAdmission(acquisition, params) {
|
|
|
22
22
|
try {
|
|
23
23
|
rawSnapshot = readSidecarLockRawSnapshotSync(acquisition.lockPath, {
|
|
24
24
|
rejectNonFile: true,
|
|
25
|
-
onOpenFailure: (
|
|
25
|
+
onOpenFailure: () => {
|
|
26
26
|
lockFileOpenDenied = true;
|
|
27
27
|
assertUnheld();
|
|
28
28
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
|
|
2
3
|
import { acquireFileLockSyncWithRoot } from "./file-lock-sync-root-acquire.js";
|
|
3
4
|
import { withSyncHeldLockHandle } from "./file-lock-sync-root-held.js";
|
|
4
5
|
import path from "node:path";
|
|
@@ -227,6 +228,13 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
227
228
|
const noFollow = process.platform !== "win32" && typeof fs.constants.O_NOFOLLOW === "number"
|
|
228
229
|
? fs.constants.O_NOFOLLOW
|
|
229
230
|
: 0;
|
|
231
|
+
try {
|
|
232
|
+
assertExclusiveCreateLeaf(lockPath);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
exclusiveCreateConflict = error.code === "EEXIST";
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
230
238
|
try {
|
|
231
239
|
fd = fs.openSync(lockPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow, 0o600);
|
|
232
240
|
}
|
|
@@ -12,6 +12,7 @@ import { prepareSecretFileWrite } from "./secret-file.js";
|
|
|
12
12
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
13
13
|
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
14
14
|
import { readRegularFile } from "./regular-file.js";
|
|
15
|
+
import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
|
|
15
16
|
import { errorCauseOptions } from "./root-errors.js";
|
|
16
17
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
17
18
|
export async function ensureParentInRoot(scopedRoot, relativePath, mode) {
|
|
@@ -67,6 +68,7 @@ export async function writeStreamToTempSource(params) {
|
|
|
67
68
|
let handle = null;
|
|
68
69
|
let handleClosedByStream = false;
|
|
69
70
|
try {
|
|
71
|
+
assertExclusiveCreateLeaf(filePath);
|
|
70
72
|
handle = await fs.open(filePath, "wx", params.mode);
|
|
71
73
|
const writable = handle.createWriteStream();
|
|
72
74
|
writable.once("close", () => {
|
|
@@ -8,8 +8,9 @@ import { ensureParentSync, ensureStoreDirectorySync, } from "./file-store-bounda
|
|
|
8
8
|
import { assertSyncStoreDirectoryReceipt } from "./file-store-sync-directory.js";
|
|
9
9
|
import { isPathInside } from "./path.js";
|
|
10
10
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { writeTempFile } from "./replace-file-descriptor.js";
|
|
12
|
+
import { AtomicIo, runSync } from "./atomic-io.js";
|
|
13
|
+
import { AtomicTempOwner } from "./replace-file-temp-owner.js";
|
|
13
14
|
import { errorCauseOptions } from "./root-errors.js";
|
|
14
15
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
15
16
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
@@ -75,35 +76,36 @@ export function writeFileSyncAtomic(params) {
|
|
|
75
76
|
});
|
|
76
77
|
}
|
|
77
78
|
const tempPath = path.join(parentGuard.dir, `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
|
|
78
|
-
const
|
|
79
|
+
const io = AtomicIo.sync(fs);
|
|
80
|
+
const owner = new AtomicTempOwner(tempPath, io);
|
|
79
81
|
let originalFailure;
|
|
80
82
|
try {
|
|
81
83
|
getFsSafeTestHooks()?.beforeFileStoreSyncPrivateWrite?.(filePath);
|
|
82
84
|
assertSyncStoreDirectoryReceipt(parentGuard);
|
|
83
85
|
owner.start();
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
io.fchmodSync = (descriptor, mode) => {
|
|
87
|
+
try {
|
|
88
|
+
fs.fchmodSync(descriptor, mode);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Best-effort on platforms that do not enforce POSIX modes.
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const temp = runSync(writeTempFile(io, {
|
|
95
|
+
tempPath, content: params.content, mode: params.mode, sync: false,
|
|
86
96
|
onIdentity: owner.onIdentity,
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
fs.fchmodSync(descriptor, mode);
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
// Best-effort on platforms that do not enforce POSIX modes.
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
});
|
|
97
|
+
}));
|
|
96
98
|
owner.adopt(temp);
|
|
97
99
|
// Preserve the store's strict fsync errors; the generic temp helper tolerates EPERM.
|
|
98
100
|
if (params.durable)
|
|
99
|
-
fs.fsyncSync(temp.fd);
|
|
100
|
-
verifyStoreFile(temp.fd, owner.identity, tempPath);
|
|
101
|
+
fs.fsyncSync(temp.file.fd);
|
|
102
|
+
verifyStoreFile(temp.file.fd, owner.identity, tempPath);
|
|
101
103
|
assertSyncStoreDirectoryReceipt(parentGuard);
|
|
102
104
|
fs.renameSync(tempPath, filePath);
|
|
103
105
|
owner.markRenamed();
|
|
104
106
|
assertSyncStoreDirectoryReceipt(parentGuard);
|
|
105
107
|
try {
|
|
106
|
-
verifyStoreFile(temp.fd, owner.identity, filePath);
|
|
108
|
+
verifyStoreFile(temp.file.fd, owner.identity, filePath);
|
|
107
109
|
}
|
|
108
110
|
catch (error) {
|
|
109
111
|
if (error instanceof FsSafeError) {
|
|
@@ -123,6 +125,6 @@ export function writeFileSyncAtomic(params) {
|
|
|
123
125
|
throw error;
|
|
124
126
|
}
|
|
125
127
|
finally {
|
|
126
|
-
owner.finish({
|
|
128
|
+
runSync(owner.finish({ originalFailure, throwOnCleanupError: false }));
|
|
127
129
|
}
|
|
128
130
|
}
|
|
@@ -8,7 +8,7 @@ import { writeFileSyncAtomic } from "./file-store-sync-write.js";
|
|
|
8
8
|
import { createJsonStore } from "./json-document-store.js";
|
|
9
9
|
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
10
10
|
import { isNotFoundPathError } from "./path.js";
|
|
11
|
-
import { throwFsSafeReadError } from "./root-errors.js";
|
|
11
|
+
import { errorCauseOptions, throwFsSafeReadError } from "./root-errors.js";
|
|
12
12
|
import { root } from "./root.js";
|
|
13
13
|
import { DEFAULT_ROOT_MAX_BYTES } from "./root-impl.js";
|
|
14
14
|
import { matchRootFileOpenFailure, openRootFileSync } from "./root-file.js";
|
|
@@ -38,9 +38,7 @@ function handleSyncStoreReadOpenFailure(opened) {
|
|
|
38
38
|
if (isNotFound(failure.error)) {
|
|
39
39
|
return null;
|
|
40
40
|
}
|
|
41
|
-
throw new FsSafeError("path-mismatch", "store target changed during read",
|
|
42
|
-
cause: failure.error instanceof Error ? failure.error : undefined,
|
|
43
|
-
});
|
|
41
|
+
throw new FsSafeError("path-mismatch", "store target changed during read", errorCauseOptions(failure.error));
|
|
44
42
|
},
|
|
45
43
|
validation: (failure) => {
|
|
46
44
|
if (failure.error instanceof FsSafeError) {
|
|
@@ -48,15 +46,11 @@ function handleSyncStoreReadOpenFailure(opened) {
|
|
|
48
46
|
}
|
|
49
47
|
// Validation failures mean the path existed but violated store policy
|
|
50
48
|
// (directory, hardlink, symlink race). Do not report them as missing.
|
|
51
|
-
throw new FsSafeError("path-mismatch", "store target failed read validation",
|
|
52
|
-
cause: failure.error instanceof Error ? failure.error : undefined,
|
|
53
|
-
});
|
|
49
|
+
throw new FsSafeError("path-mismatch", "store target failed read validation", errorCauseOptions(failure.error));
|
|
54
50
|
},
|
|
55
51
|
io: (failure) => throwFsSafeReadError(failure.error, "store"),
|
|
56
52
|
fallback: (failure) => {
|
|
57
|
-
throw new FsSafeError("path-mismatch", "store target changed during read",
|
|
58
|
-
cause: failure.error instanceof Error ? failure.error : undefined,
|
|
59
|
-
});
|
|
53
|
+
throw new FsSafeError("path-mismatch", "store target changed during read", errorCauseOptions(failure.error));
|
|
60
54
|
},
|
|
61
55
|
});
|
|
62
56
|
}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Returns true when `fs.stat()` can stat the path.
|
|
3
|
-
*
|
|
4
|
-
* This follows stat semantics: broken symlinks return false, while symlinks to
|
|
5
|
-
* existing targets return true.
|
|
6
|
-
*/
|
|
1
|
+
/** True if stat succeeds; follows symlinks, so broken links return false. */
|
|
7
2
|
export declare function pathExists(filePath: string): Promise<boolean>;
|
|
8
|
-
/**
|
|
9
|
-
* Synchronous counterpart to `pathExists()`, with the same `fs.statSync()`
|
|
10
|
-
* semantics.
|
|
11
|
-
*/
|
|
3
|
+
/** Synchronous {@link pathExists}. */
|
|
12
4
|
export declare function pathExistsSync(filePath: string): boolean;
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { pathForWindowsFilesystem } from "./windows-path-alias.js";
|
|
3
|
-
/**
|
|
4
|
-
* Returns true when `fs.stat()` can stat the path.
|
|
5
|
-
*
|
|
6
|
-
* This follows stat semantics: broken symlinks return false, while symlinks to
|
|
7
|
-
* existing targets return true.
|
|
8
|
-
*/
|
|
3
|
+
/** True if stat succeeds; follows symlinks, so broken links return false. */
|
|
9
4
|
export async function pathExists(filePath) {
|
|
10
5
|
try {
|
|
11
6
|
fs.statSync(pathForWindowsFilesystem(filePath));
|
|
@@ -15,10 +10,7 @@ export async function pathExists(filePath) {
|
|
|
15
10
|
return false;
|
|
16
11
|
}
|
|
17
12
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Synchronous counterpart to `pathExists()`, with the same `fs.statSync()`
|
|
20
|
-
* semantics.
|
|
21
|
-
*/
|
|
13
|
+
/** Synchronous {@link pathExists}. */
|
|
22
14
|
export function pathExistsSync(filePath) {
|
|
23
15
|
try {
|
|
24
16
|
fs.statSync(pathForWindowsFilesystem(filePath));
|
|
@@ -2,10 +2,8 @@ import { type RootBoundaryIdentity } from "./root-boundary.js";
|
|
|
2
2
|
import { type MutationDirectoryObservation } from "./pinned-mutation-observation.js";
|
|
3
3
|
import type { PinnedCreatedDirectoryReceipt, PinnedMutationAdmissionReceipt, PinnedMutationAuthorizationToken } from "./pinned-write-types.js";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* component so callers can guard/use that path directly instead of
|
|
8
|
-
* re-deriving it from the original, possibly-symlinked, lexical path.
|
|
5
|
+
* Guard each component from rootReal to targetPath; optionally create missing ones.
|
|
6
|
+
* Returns the resolved path: use it, rather than the lexical input, for later guards.
|
|
9
7
|
*/
|
|
10
8
|
export declare function mkdirPathComponentsWithGuards(params: {
|
|
11
9
|
rootReal: string;
|
|
@@ -15,6 +13,7 @@ export declare function mkdirPathComponentsWithGuards(params: {
|
|
|
15
13
|
beforeUseComponent?: (componentPath: string, prospectiveTargetPath: string, retainedTargetPath: string | undefined) => Promise<void> | void;
|
|
16
14
|
afterCreateComponent?: (receipt: PinnedCreatedDirectoryReceipt) => PinnedMutationAuthorizationToken | undefined;
|
|
17
15
|
assertBeforeMutation?: () => void;
|
|
16
|
+
createMissing?: boolean;
|
|
18
17
|
mode?: number;
|
|
19
18
|
private?: boolean;
|
|
20
19
|
rejectSymlinks?: boolean;
|
|
@@ -10,7 +10,7 @@ import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservi
|
|
|
10
10
|
import { realpathSync } from "./realpath.js";
|
|
11
11
|
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
12
12
|
import { checkedMutationDirectory } from "./pinned-mutation-observation.js";
|
|
13
|
-
import { createPathSegmentRoute, joinPathSegmentRoute, } from "./path-segment-route.js";
|
|
13
|
+
import { createPathSegmentRoute, joinPathSegmentRoute, sameAbsolutePath, } from "./path-segment-route.js";
|
|
14
14
|
function sameDirectoryFacts(left, right) {
|
|
15
15
|
return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode &&
|
|
16
16
|
left.nlink === right.nlink;
|
|
@@ -55,9 +55,6 @@ function createdDirectoryEvidence(admission, parent, childPath) {
|
|
|
55
55
|
return undefined;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
function sameAbsolutePath(left, right) {
|
|
59
|
-
return path.relative(path.resolve(left), path.resolve(right)) === "";
|
|
60
|
-
}
|
|
61
58
|
function suppliedExactRootIdentity(identity) {
|
|
62
59
|
return typeof identity?.dev === "bigint" && typeof identity.ino === "bigint"
|
|
63
60
|
? { dev: identity.dev, ino: identity.ino }
|
|
@@ -87,10 +84,8 @@ async function realpathOrThrowNotFile(target) {
|
|
|
87
84
|
}
|
|
88
85
|
}
|
|
89
86
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* component so callers can guard/use that path directly instead of
|
|
93
|
-
* re-deriving it from the original, possibly-symlinked, lexical path.
|
|
87
|
+
* Guard each component from rootReal to targetPath; optionally create missing ones.
|
|
88
|
+
* Returns the resolved path: use it, rather than the lexical input, for later guards.
|
|
94
89
|
*/
|
|
95
90
|
export async function mkdirPathComponentsWithGuards(params) {
|
|
96
91
|
const rawRootReal = params.rootReal;
|
|
@@ -181,7 +176,7 @@ export async function mkdirPathComponentsWithGuards(params) {
|
|
|
181
176
|
}
|
|
182
177
|
}
|
|
183
178
|
}
|
|
184
|
-
if (shouldCreate) {
|
|
179
|
+
if (shouldCreate && params.createMissing !== false) {
|
|
185
180
|
params.assertBeforeMutation?.();
|
|
186
181
|
// Both policy and ordinary mkdir must renew the parent after callbacks.
|
|
187
182
|
inspectGuardCurrent(parentGuard);
|
|
@@ -244,15 +239,8 @@ export async function mkdirPathComponentsWithGuards(params) {
|
|
|
244
239
|
}
|
|
245
240
|
const admittedNextPath = admittedNextReal.path;
|
|
246
241
|
if (stat?.isSymbolicLink()) {
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// verified above that it resolves inside the root, so treat the
|
|
250
|
-
// resolved real path as the directory for the rest of this walk
|
|
251
|
-
// instead of rejecting it outright. Guard checks from here on operate
|
|
252
|
-
// on the real (non-symlink) path, preserving TOCTOU protection for
|
|
253
|
-
// every subsequent segment. Callers that need the final directory
|
|
254
|
-
// (e.g. to guard it themselves after this function returns) must use
|
|
255
|
-
// the returned resolved path, not their own lexical parent path.
|
|
242
|
+
// Containment was checked above. Continue through the symlink's real directory
|
|
243
|
+
// so subsequent guards bind that directory.
|
|
256
244
|
const targetStat = fsSync.statSync(admittedNextPath);
|
|
257
245
|
if (!targetStat.isDirectory()) {
|
|
258
246
|
throw directoryComponentNotDirectoryError();
|
|
@@ -33,7 +33,7 @@ export const GUEST_FILESYSTEM_DISPATCH_PYTHON = [
|
|
|
33
33
|
" sys.exit(SANDBOX_CREATE_EXISTS_EXIT_CODE)",
|
|
34
34
|
" elif operation == 'read':",
|
|
35
35
|
" if len(sys.argv) > 5:",
|
|
36
|
-
"
|
|
36
|
+
" read_file(parent_fd, sys.argv[4], int(sys.argv[5]))",
|
|
37
37
|
" else:",
|
|
38
38
|
" read_file(parent_fd, sys.argv[4])",
|
|
39
39
|
" elif operation == 'readdir':",
|
|
@@ -42,7 +42,6 @@ export const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON = [
|
|
|
42
42
|
" flags = 0x00000004 # RENAME_EXCL",
|
|
43
43
|
" else:",
|
|
44
44
|
" rename_fn = None",
|
|
45
|
-
" flags = 0",
|
|
46
45
|
" if rename_fn is not None:",
|
|
47
46
|
" rename_fn.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]",
|
|
48
47
|
" rename_fn.restype = ctypes.c_int",
|
|
@@ -77,7 +76,6 @@ export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
|
|
|
77
76
|
" staging_name = None",
|
|
78
77
|
" temp_fd = None",
|
|
79
78
|
" temp_name = 'payload'",
|
|
80
|
-
" temp_inode = None",
|
|
81
79
|
" try:",
|
|
82
80
|
" try:",
|
|
83
81
|
" os.lstat(basename, dir_fd=parent_fd)",
|
|
@@ -204,7 +204,9 @@ export const GUEST_FILESYSTEM_PYTHON = [
|
|
|
204
204
|
"",
|
|
205
205
|
GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON,
|
|
206
206
|
"",
|
|
207
|
-
"def
|
|
207
|
+
"def read_file(parent_fd, basename, max_bytes=None):",
|
|
208
|
+
" if max_bytes is not None and max_bytes < 0:",
|
|
209
|
+
" raise OSError(errno.EINVAL, 'read limit must be non-negative', basename)",
|
|
208
210
|
" file_fd = os.open(basename, READ_FLAGS, dir_fd=parent_fd)",
|
|
209
211
|
" try:",
|
|
210
212
|
" file_stat = os.fstat(file_fd)",
|
|
@@ -227,14 +229,6 @@ export const GUEST_FILESYSTEM_PYTHON = [
|
|
|
227
229
|
" finally:",
|
|
228
230
|
" os.close(file_fd)",
|
|
229
231
|
"",
|
|
230
|
-
"def read_file(parent_fd, basename):",
|
|
231
|
-
" read_file_impl(parent_fd, basename, None)",
|
|
232
|
-
"",
|
|
233
|
-
"def read_file_bounded(parent_fd, basename, max_bytes):",
|
|
234
|
-
" if max_bytes < 0:",
|
|
235
|
-
" raise OSError(errno.EINVAL, 'read limit must be non-negative', basename)",
|
|
236
|
-
" read_file_impl(parent_fd, basename, max_bytes)",
|
|
237
|
-
"",
|
|
238
232
|
"def remove_tree(parent_fd, basename):",
|
|
239
233
|
" entry_stat = os.lstat(basename, dir_fd=parent_fd)",
|
|
240
234
|
" if not stat.S_ISDIR(entry_stat.st_mode) or stat.S_ISLNK(entry_stat.st_mode):",
|
|
@@ -2,7 +2,7 @@ export { FsSafeError, categorizeFsSafeError, } from "./errors.js";
|
|
|
2
2
|
export type * from "./errors.js";
|
|
3
3
|
export { DEFAULT_ROOT_MAX_BYTES, root, type Root, } from "./root.js";
|
|
4
4
|
export type * from "./root-public-types.js";
|
|
5
|
-
export {
|
|
5
|
+
export { configureFsSafeNative, getFsSafeNativeConfig, } from "./native-config.js";
|
|
6
6
|
export type * from "./config.js";
|
|
7
7
|
export { writeExternalFileWithinRoot, } from "./output.js";
|
|
8
8
|
export type * from "./output.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { FsSafeError, categorizeFsSafeError, } from "./errors.js";
|
|
2
2
|
export { DEFAULT_ROOT_MAX_BYTES, root, } from "./root.js";
|
|
3
|
-
export {
|
|
3
|
+
export { configureFsSafeNative, getFsSafeNativeConfig, } from "./native-config.js";
|
|
4
4
|
export { writeExternalFileWithinRoot, } from "./output.js";
|
|
5
5
|
export { configureFsSafeLocks, getFsSafeLockConfig, } from "./lock-config.js";
|
|
@@ -45,11 +45,7 @@ class QueueDirectory {
|
|
|
45
45
|
throw new Error(`durable queue path is not a directory: ${dir}`);
|
|
46
46
|
}
|
|
47
47
|
const segments = path.relative(base, target).split(path.sep).filter(Boolean);
|
|
48
|
-
for (
|
|
49
|
-
const segment = segments[index];
|
|
50
|
-
if (segment === undefined) {
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
48
|
+
for (const segment of segments) {
|
|
53
49
|
current = path.join(current, segment);
|
|
54
50
|
let stat;
|
|
55
51
|
try {
|
|
@@ -7,8 +7,9 @@ import { stringifyJsonDocument } from "./json-stringify.js";
|
|
|
7
7
|
import { readRegularFile, readRegularFileSync, statRegularFile } from "./regular-file.js";
|
|
8
8
|
import { openRootFileSync } from "./root-file.js";
|
|
9
9
|
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { writeTempFile, syncDirectoryBestEffort } from "./replace-file-descriptor.js";
|
|
11
|
+
import { AtomicIo, runSync } from "./atomic-io.js";
|
|
12
|
+
import { AtomicTempOwner } from "./replace-file-temp-owner.js";
|
|
12
13
|
import { admitStandalonePublicationPath, assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
13
14
|
import { writeTextAtomic } from "./text-atomic.js";
|
|
14
15
|
import { sleep } from "./timing.js";
|
|
@@ -57,28 +58,8 @@ function trySetSecureMode(fd) {
|
|
|
57
58
|
// Best-effort mode application stays on the retained staging descriptor.
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
function trySyncDirectory(pathname) {
|
|
61
|
-
let fd;
|
|
62
|
-
try {
|
|
63
|
-
fd = fsSync.openSync(path.dirname(pathname), "r");
|
|
64
|
-
fsSync.fsyncSync(fd);
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
// best-effort; some platforms/filesystems do not support syncing directories.
|
|
68
|
-
}
|
|
69
|
-
finally {
|
|
70
|
-
if (fd !== undefined) {
|
|
71
|
-
try {
|
|
72
|
-
fsSync.closeSync(fd);
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
// best-effort cleanup
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
61
|
function renameJsonFileWithFallback(owner, pathname) {
|
|
81
|
-
owner.assertCurrent(
|
|
62
|
+
runSync(owner.assertCurrent());
|
|
82
63
|
try {
|
|
83
64
|
fsSync.renameSync(owner.pathname, pathname);
|
|
84
65
|
return;
|
|
@@ -86,9 +67,9 @@ function renameJsonFileWithFallback(owner, pathname) {
|
|
|
86
67
|
catch (error) {
|
|
87
68
|
const code = error?.code;
|
|
88
69
|
if (code === "EPERM" || code === "EEXIST") {
|
|
89
|
-
owner.assertCurrent(
|
|
70
|
+
runSync(owner.assertCurrent());
|
|
90
71
|
fsSync.rmSync(pathname, { force: true });
|
|
91
|
-
owner.assertCurrent(
|
|
72
|
+
runSync(owner.assertCurrent());
|
|
92
73
|
fsSync.renameSync(owner.pathname, pathname);
|
|
93
74
|
return;
|
|
94
75
|
}
|
|
@@ -113,31 +94,32 @@ export function writeJsonSync(pathname, data) {
|
|
|
113
94
|
const tmpPath = path.format({ ...path.parse(filePath), base: `.fs-safe-${randomUUID()}.tmp` });
|
|
114
95
|
const payload = `${stringifyJsonDocument(data, null, 2)}\n`;
|
|
115
96
|
fsSync.mkdirSync(recursiveMkdirPath(path.dirname(filePath)), { recursive: true, mode: JSON_DIR_MODE });
|
|
116
|
-
const
|
|
97
|
+
const io = AtomicIo.sync(fsSync);
|
|
98
|
+
const owner = new AtomicTempOwner(tmpPath, io);
|
|
117
99
|
let originalFailure;
|
|
118
100
|
try {
|
|
119
101
|
owner.start();
|
|
120
|
-
const temp =
|
|
121
|
-
|
|
102
|
+
const temp = runSync(writeTempFile(io, {
|
|
103
|
+
tempPath: tmpPath, content: payload, mode: JSON_FILE_MODE,
|
|
122
104
|
sync: false, onIdentity: owner.onIdentity,
|
|
123
|
-
});
|
|
105
|
+
}));
|
|
124
106
|
owner.adopt(temp);
|
|
125
|
-
owner.assertCurrent(
|
|
126
|
-
trySetSecureMode(temp.fd);
|
|
127
|
-
fsSync.fsyncSync(temp.fd);
|
|
107
|
+
runSync(owner.assertCurrent());
|
|
108
|
+
trySetSecureMode(temp.file.fd);
|
|
109
|
+
fsSync.fsyncSync(temp.file.fd);
|
|
128
110
|
renameJsonFileWithFallback(owner, filePath);
|
|
129
111
|
owner.markRenamed();
|
|
130
|
-
owner.assertPublished(
|
|
131
|
-
trySetSecureMode(temp.fd);
|
|
132
|
-
|
|
133
|
-
owner.assertPublished(
|
|
112
|
+
runSync(owner.assertPublished(filePath));
|
|
113
|
+
trySetSecureMode(temp.file.fd);
|
|
114
|
+
runSync(syncDirectoryBestEffort(io, path.dirname(filePath)));
|
|
115
|
+
runSync(owner.assertPublished(filePath));
|
|
134
116
|
}
|
|
135
117
|
catch (error) {
|
|
136
118
|
originalFailure = { error };
|
|
137
119
|
throw error;
|
|
138
120
|
}
|
|
139
121
|
finally {
|
|
140
|
-
owner.finish({
|
|
122
|
+
runSync(owner.finish({ originalFailure, throwOnCleanupError: false }));
|
|
141
123
|
}
|
|
142
124
|
}
|
|
143
125
|
export class JsonFileReadError extends Error {
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import fsSync, { constants as fsConstants } from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
|
|
5
6
|
import { assertSyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
6
7
|
import { FsSafeError } from "./errors.js";
|
|
7
8
|
import { guardedRename } from "./guarded-mutation.js";
|
|
@@ -142,6 +143,7 @@ async function copyRegularFilePinned(params) {
|
|
|
142
143
|
}
|
|
143
144
|
await assertSourceStillMatches(params.from, openedIdentity);
|
|
144
145
|
const parentGuard = await createAsyncDirectoryGuard(path.dirname(params.to), { bigint: true });
|
|
146
|
+
assertExclusiveCreateLeaf(params.to);
|
|
145
147
|
const destinationHandle = await fs.open(params.to, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, modeBits(params.mode) || 0o666);
|
|
146
148
|
let destinationIdentity;
|
|
147
149
|
try {
|
|
@@ -3,6 +3,7 @@ export declare class MutationAuthorityError extends FsSafeError {
|
|
|
3
3
|
readonly rejection: unknown;
|
|
4
4
|
constructor(rejection: unknown);
|
|
5
5
|
}
|
|
6
|
+
export declare function isMutationAuthorityError(error: unknown): error is MutationAuthorityError;
|
|
6
7
|
export declare function assertSynchronousCallbackResult(returned: unknown, name: string): void;
|
|
7
8
|
export declare function composeMutationAssertions(defaultAssertion?: () => void, callAssertion?: () => void): (() => void) | undefined;
|
|
8
9
|
export declare function rethrowMutationAuthorityError(error: unknown): never;
|
|
@@ -7,6 +7,14 @@ export class MutationAuthorityError extends FsSafeError {
|
|
|
7
7
|
this.rejection = rejection;
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
+
export function isMutationAuthorityError(error) {
|
|
11
|
+
try {
|
|
12
|
+
return error instanceof MutationAuthorityError;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
10
18
|
export function assertSynchronousCallbackResult(returned, name) {
|
|
11
19
|
if (returned !== null &&
|
|
12
20
|
(typeof returned === "object" || typeof returned === "function") &&
|
|
@@ -34,7 +42,7 @@ export function composeMutationAssertions(defaultAssertion, callAssertion) {
|
|
|
34
42
|
};
|
|
35
43
|
}
|
|
36
44
|
export function rethrowMutationAuthorityError(error) {
|
|
37
|
-
if (error
|
|
45
|
+
if (isMutationAuthorityError(error))
|
|
38
46
|
throw error.rejection;
|
|
39
47
|
// Cleanup failures and indeterminate publication receipts must retain their context.
|
|
40
48
|
throw error;
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import type { RetainedFileResult } from "./retained-file-types.js";
|
|
2
|
+
export type NativePublicationTransition = {
|
|
3
|
+
outcome: "committed" | "not-published" | "indeterminate";
|
|
4
|
+
errorCode?: string;
|
|
5
|
+
errorMessage?: string;
|
|
6
|
+
};
|
|
7
|
+
export interface NativeWindowsEntryPublication {
|
|
8
|
+
readonly admission: {
|
|
9
|
+
outcome: string;
|
|
10
|
+
errorCode?: string;
|
|
11
|
+
errorMessage?: string;
|
|
12
|
+
};
|
|
13
|
+
current(published: boolean): void;
|
|
14
|
+
publish(): NativePublicationTransition;
|
|
15
|
+
close(): {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
}[];
|
|
19
|
+
}
|
|
2
20
|
export interface NativeRetainedFile {
|
|
3
21
|
readonly admission: RetainedFileResult | {
|
|
4
22
|
status: "retained";
|
|
@@ -63,6 +81,17 @@ export interface NativeOwnedTreeRemovalResult {
|
|
|
63
81
|
errorCode?: string;
|
|
64
82
|
errorMessage?: string;
|
|
65
83
|
}
|
|
84
|
+
export interface NativeRootRemovalEntry {
|
|
85
|
+
dev: bigint;
|
|
86
|
+
ino: bigint;
|
|
87
|
+
directory: boolean;
|
|
88
|
+
symlink: boolean;
|
|
89
|
+
}
|
|
90
|
+
export interface NativeRootRemovalDirectory {
|
|
91
|
+
readonly fd: number;
|
|
92
|
+
read(): string | null;
|
|
93
|
+
close(): void;
|
|
94
|
+
}
|
|
66
95
|
export interface NativeWindowsAccessControlEntry {
|
|
67
96
|
sid: string;
|
|
68
97
|
mask: number;
|
|
@@ -103,10 +132,19 @@ type NativeTwoPathArgs = [
|
|
|
103
132
|
targetRelPath: string
|
|
104
133
|
];
|
|
105
134
|
export interface NativeBinding {
|
|
135
|
+
rootRemovalStat?(parent: number, name: string): NativeRootRemovalEntry;
|
|
136
|
+
rootRemovalUnlink?(parent: number, name: string, dev: bigint, ino: bigint, directory: boolean): void;
|
|
137
|
+
openRootRemovalDirectory?(parent: number, name: string, dev: bigint, ino: bigint): NativeRootRemovalDirectory;
|
|
138
|
+
openCreateBeneath?(parentFd: number, basename: string, flags: number, mode: number): number;
|
|
139
|
+
renameReplaceWithIdentity?(sourceParent: number, source: string, targetParent: number, target: string, dev: bigint, ino: bigint): void;
|
|
106
140
|
/** Windows-only, private handle custody; no borrowed/runtime descriptors. */
|
|
107
141
|
retainWindowsFile?(directory: string, basename: string, parentDev: bigint, parentIno: bigint, dev: bigint, ino: bigint, size: bigint, mtimeNs: bigint, ctimeNs: bigint, sha256: string, maxBytes: number): NativeRetainedFile;
|
|
108
142
|
watchRegister?(root: string, limit: number, callback: (batch: import("./watch-native.js").NativeWatchWireBatch) => void, persistent: boolean): number;
|
|
109
143
|
watchConfigure?(id: number, anchors: string[], exclusions: string[]): void;
|
|
144
|
+
watchEntries?(id: number, entries: import("./watch-native.js").NativeWatchEntry[]): {
|
|
145
|
+
directories: number;
|
|
146
|
+
changed: boolean;
|
|
147
|
+
};
|
|
110
148
|
watchAdd?(id: number, directory: {
|
|
111
149
|
root: string;
|
|
112
150
|
relative: string;
|
|
@@ -164,7 +202,7 @@ export interface NativeBinding {
|
|
|
164
202
|
createStagedFile?(parentFd: number, basename: string): number;
|
|
165
203
|
stagedFileMatches?(parentFd: number, basename: string, fileFd: number): boolean;
|
|
166
204
|
removeStagedFile?(parentFd: number, basename: string, fileFd: number): "removed" | "name-absent" | "preserved";
|
|
167
|
-
copyFileExclusive?(sourceFd: number, parentFd: number, basename: string, clone: CopyCloneMode, maxBytes: number | undefined, signal: AbortSignal | undefined
|
|
205
|
+
copyFileExclusive?(sourceFd: number, parentFd: number, basename: string, clone: CopyCloneMode, maxBytes: number | undefined, signal: AbortSignal | undefined): Promise<NativeFileCopyResult>;
|
|
168
206
|
cloneFileExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): number;
|
|
169
207
|
copyFileRangeExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): Promise<NativeCopyResult>;
|
|
170
208
|
createPrivateDirectory(path: string): void;
|
|
@@ -180,15 +218,22 @@ export interface NativeBinding {
|
|
|
180
218
|
linkBeneath(...args: NativeTwoPathArgs): void;
|
|
181
219
|
/** Direct-child mkdir; true is receipt provenance only, never cleanup ownership. */
|
|
182
220
|
mkdirChildBeneath?(parentFd: number, basename: string, mode: number): boolean;
|
|
221
|
+
mkdirOpenChildBeneath?(parentFd: number, basename: string, mode: number, flags: number): {
|
|
222
|
+
fd: number;
|
|
223
|
+
created: boolean;
|
|
224
|
+
};
|
|
183
225
|
mkdirBeneath(rootFd: number, relPath: string, mode: number): void;
|
|
184
226
|
openBeneath(rootFd: number, relPath: string, flags: number): NativeOpenBeneathResult;
|
|
185
|
-
readArchiveEntryNative(path: string, kind: string, requested: string, maxBytes: number, limits: TarMeterLimits, signal: AbortSignal): Promise<Buffer>;
|
|
186
227
|
readOwnerAndDacl(path: string): NativeWindowsSecurityFacts;
|
|
187
228
|
/** Internal Windows-only inspection of the exact borrowed Node descriptor. */
|
|
188
229
|
inspectWindowsSecureFileHandle?(fd: number): NativeWindowsDescriptorSecurityFacts;
|
|
189
230
|
ownedTreeRemovalAvailable?(parentFd: number): boolean;
|
|
190
231
|
removeOwnedTree?(parentFd: number, basename: string, directoryFd: number): Promise<NativeOwnedTreeRemovalResult>;
|
|
191
232
|
removeOwnedTreeSync?(parentFd: number, basename: string, directoryFd: number): NativeOwnedTreeRemovalResult;
|
|
233
|
+
/** Local filesystem admission for one-way cooperative entry publication. */
|
|
234
|
+
retainWindowsEntryPublication?(sourcePath: string, name: string, sourceDev: bigint, sourceIno: bigint, targetPath: string, targetName: string, targetDev: bigint, targetIno: bigint, dev: bigint, ino: bigint, kind: "file" | "directory" | "symlink"): NativeWindowsEntryPublication;
|
|
235
|
+
entryPublicationFilesystem?(parentFd: number): string;
|
|
236
|
+
publishRetainedEntryNoReplace?(sourceParentFd: number, sourceBasename: string, sourceFd: number, targetParentFd: number, targetBasename: string): NativePublicationTransition;
|
|
192
237
|
renameNoReplace(...args: NativeTwoPathArgs): void;
|
|
193
238
|
/** Identity-fenced retained-directory rename capability. */
|
|
194
239
|
renameNoReplaceWithIdentity?(...args: [...paths: NativeTwoPathArgs, expectedSourceDev: bigint, expectedSourceIno: bigint]): void;
|
|
@@ -2,15 +2,7 @@ export type FsSafeNativeMode = "auto" | "off" | "require";
|
|
|
2
2
|
export type FsSafeNativeConfig = {
|
|
3
3
|
mode: FsSafeNativeMode;
|
|
4
4
|
};
|
|
5
|
-
/** @deprecated Compatibility bridge for 0.4 upgrades. Use {@link FsSafeNativeConfig}. */
|
|
6
|
-
export type FsSafePythonConfig = {
|
|
7
|
-
mode: FsSafeNativeMode;
|
|
8
|
-
pythonPath?: string;
|
|
9
|
-
};
|
|
10
5
|
export declare function configureFsSafeNative(config: Partial<FsSafeNativeConfig>): void;
|
|
11
|
-
/**
|
|
12
|
-
* @deprecated Compatibility bridge for 0.4 upgrades. Use configureFsSafeNative.
|
|
13
|
-
*/
|
|
14
|
-
export declare function configureFsSafePython(config: Partial<FsSafePythonConfig>): void;
|
|
15
6
|
export declare function getFsSafeNativeConfig(): FsSafeNativeConfig;
|
|
7
|
+
export declare function isFsSafeNativeRequired(): boolean;
|
|
16
8
|
export declare function __resetFsSafeNativeConfigForTest(): void;
|