@openclaw/feishu 2026.9.8 → 2026.10.1-beta.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.
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
|
|
2
3
|
import fs, {} from "node:fs";
|
|
3
4
|
import fsAsync, {} from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
@@ -47,6 +48,7 @@ function createPrivateWindowsFile(selected, options, mode) {
|
|
|
47
48
|
}
|
|
48
49
|
assertSynchronousCallbackResult(assertion?.(), "assertBeforeMutation");
|
|
49
50
|
assertStage();
|
|
51
|
+
assertExclusiveCreateLeaf(stagePath);
|
|
50
52
|
file = ownFileDescriptorSync(fs.openSync(stagePath, fs.constants.O_RDWR | fs.constants.O_CREAT |
|
|
51
53
|
fs.constants.O_EXCL | resolveReadOpenFlags(), 0o600));
|
|
52
54
|
identity = assertCreationFile(file.fd, stagePath);
|
|
@@ -105,10 +107,10 @@ function createPrivateWindowsFile(selected, options, mode) {
|
|
|
105
107
|
throw privateFileSettlementFailure({ primary, cleanup, publication, path: selected.target, stageDirectory });
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
|
-
function
|
|
110
|
+
export function createFileSync(targetPath, options = {}) {
|
|
109
111
|
const permissions = resolveCreationPermissions(options, false);
|
|
110
112
|
const assertion = options.assertBeforeMutation;
|
|
111
|
-
const selected = prepareCreationPath(targetPath
|
|
113
|
+
const selected = prepareCreationPath(targetPath);
|
|
112
114
|
if (permissions.private && process.platform === "win32") {
|
|
113
115
|
assertPrivateFileCreationAvailable();
|
|
114
116
|
return createPrivateWindowsFile(selected, { ...permissions, assertBeforeMutation: assertion }, permissions.mode);
|
|
@@ -116,6 +118,7 @@ function createFileCore(targetPath, options, admission) {
|
|
|
116
118
|
assertBeforeCreation(selected, permissions, assertion, "file");
|
|
117
119
|
let file;
|
|
118
120
|
try {
|
|
121
|
+
assertExclusiveCreateLeaf(selected.target);
|
|
119
122
|
file = ownFileDescriptorSync(fs.openSync(selected.target, fs.constants.O_RDWR | fs.constants.O_CREAT |
|
|
120
123
|
fs.constants.O_EXCL | resolveReadOpenFlags(), permissions.mode ?? 0o666));
|
|
121
124
|
}
|
|
@@ -142,9 +145,6 @@ function createFileCore(targetPath, options, admission) {
|
|
|
142
145
|
});
|
|
143
146
|
}
|
|
144
147
|
}
|
|
145
|
-
export function createFileSync(targetPath, options = {}) {
|
|
146
|
-
return createFileCore(targetPath, options, {});
|
|
147
|
-
}
|
|
148
148
|
/** Internal async writer; Windows preparation uses the nonblocking stage owner. */
|
|
149
149
|
export async function createFileHandle(targetPath, options = {}, admission = {}) {
|
|
150
150
|
const permissions = resolveCreationPermissions(options, false);
|
|
@@ -152,6 +152,12 @@ export async function createFileHandle(targetPath, options = {}, admission = {})
|
|
|
152
152
|
if (process.platform !== "win32" || !permissions.private) {
|
|
153
153
|
const selected = prepareCreationPath(targetPath, admission.expectedParentIdentity);
|
|
154
154
|
assertBeforeCreation(selected, permissions, assertion, "file");
|
|
155
|
+
try {
|
|
156
|
+
assertExclusiveCreateLeaf(selected.target);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
throw creationCollision(error);
|
|
160
|
+
}
|
|
155
161
|
const handle = await fsAsync.open(selected.target, fs.constants.O_RDWR | fs.constants.O_CREAT |
|
|
156
162
|
fs.constants.O_EXCL | resolveReadOpenFlags(), permissions.mode ?? 0o666)
|
|
157
163
|
.catch(error => { throw creationCollision(error); });
|
|
@@ -316,6 +322,7 @@ async function createPrivateWindowsFileHandle(selected, options) {
|
|
|
316
322
|
}
|
|
317
323
|
assertSynchronousCallbackResult(assertion?.(), "assertBeforeMutation");
|
|
318
324
|
assertStage();
|
|
325
|
+
assertExclusiveCreateLeaf(stagePath);
|
|
319
326
|
file = await fsAsync.open(stagePath, fs.constants.O_RDWR | fs.constants.O_CREAT |
|
|
320
327
|
fs.constants.O_EXCL | resolveReadOpenFlags(), 0o600);
|
|
321
328
|
assertStage();
|
|
@@ -7,6 +7,11 @@ export type AsyncDirectoryGuard<T extends Stats | BigIntStats = Stats> = {
|
|
|
7
7
|
};
|
|
8
8
|
export type AnyAsyncDirectoryGuard = AsyncDirectoryGuard<Stats | BigIntStats>;
|
|
9
9
|
export type SyncDirectoryGuard = AsyncDirectoryGuard;
|
|
10
|
+
type DirectoryGuardMode = "native" | "normalized";
|
|
11
|
+
type DirectoryGuardOptions = {
|
|
12
|
+
bigint?: boolean;
|
|
13
|
+
initial?: BigIntStats;
|
|
14
|
+
};
|
|
10
15
|
export type DirectoryIdentity = Readonly<{
|
|
11
16
|
dev: bigint;
|
|
12
17
|
ino: bigint;
|
|
@@ -27,8 +32,13 @@ export declare function createAsyncDirectoryGuard(dir: string, options: {
|
|
|
27
32
|
bigint: boolean;
|
|
28
33
|
}): Promise<AnyAsyncDirectoryGuard>;
|
|
29
34
|
export declare function createSyncDirectoryGuard(dir: string): SyncDirectoryGuard;
|
|
35
|
+
export declare function captureDirectoryGuard(dir: string, mode: DirectoryGuardMode, options?: {
|
|
36
|
+
bigint?: false;
|
|
37
|
+
}): SyncDirectoryGuard;
|
|
38
|
+
export declare function captureDirectoryGuard(dir: string, mode: DirectoryGuardMode, options?: DirectoryGuardOptions): AnyAsyncDirectoryGuard;
|
|
30
39
|
export declare function assertAsyncDirectoryGuard(guard: AnyAsyncDirectoryGuard): Promise<void>;
|
|
31
40
|
export declare function assertSyncDirectoryGuard(guard: SyncDirectoryGuard | AnyAsyncDirectoryGuard): void;
|
|
41
|
+
export declare function assertDirectoryGuard(guard: AnyAsyncDirectoryGuard, mode: DirectoryGuardMode): void;
|
|
32
42
|
export declare function createNearestExistingDirectoryGuard(rootReal: string, targetPath: string): Promise<AsyncDirectoryGuard>;
|
|
33
43
|
export declare function createNearestExistingDirectoryGuard(rootReal: string, targetPath: string, options: {
|
|
34
44
|
bigint: true;
|
|
@@ -52,3 +62,4 @@ export declare function extendDirectoryObservationGuard(observation: StatObserva
|
|
|
52
62
|
export declare function inspectDirectoryObservationSync(dir: string, expected?: ExactStatIdentity): StatObservationReceipt;
|
|
53
63
|
export declare function assertDirectoryObservationSync(dir: string, expected: ExactStatIdentity): Stats | BigIntStats;
|
|
54
64
|
export declare function assertDirectoryObservationGuardSync(guard: DirectoryObservationGuard): void;
|
|
65
|
+
export {};
|
|
@@ -35,7 +35,7 @@ export async function createAsyncDirectoryGuard(dir, options) {
|
|
|
35
35
|
export function createSyncDirectoryGuard(dir) {
|
|
36
36
|
return captureDirectoryGuard(dir, "normalized");
|
|
37
37
|
}
|
|
38
|
-
function captureDirectoryGuard(dir, mode, options) {
|
|
38
|
+
export function captureDirectoryGuard(dir, mode, options) {
|
|
39
39
|
const operationPath = directoryOperationPath(dir);
|
|
40
40
|
const stat = options?.bigint
|
|
41
41
|
? inspectDirectoryIdentityAtPathSync(operationPath, undefined, options.initial)
|
|
@@ -53,7 +53,7 @@ export async function assertAsyncDirectoryGuard(guard) {
|
|
|
53
53
|
export function assertSyncDirectoryGuard(guard) {
|
|
54
54
|
assertDirectoryGuard(guard, "normalized");
|
|
55
55
|
}
|
|
56
|
-
function assertDirectoryGuard(guard, mode) {
|
|
56
|
+
export function assertDirectoryGuard(guard, mode) {
|
|
57
57
|
const dir = guard.dir;
|
|
58
58
|
const operationPath = directoryOperationPath(dir);
|
|
59
59
|
const expectedRealPath = guard.realPath;
|
|
@@ -82,16 +82,13 @@ function assertDirectoryGuard(guard, mode) {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
export async function createNearestExistingDirectoryGuard(rootReal, targetPath, options = { bigint: false }) {
|
|
85
|
-
return nearestExistingDirectoryGuard(rootReal, targetPath, "native", options);
|
|
86
|
-
}
|
|
87
|
-
function nearestExistingDirectoryGuard(rootReal, targetPath, mode, options) {
|
|
88
85
|
assertNoWindowsPathAlias(rootReal, "filesystem");
|
|
89
86
|
assertNoWindowsPathAlias(targetPath, "filesystem");
|
|
90
87
|
let current = resolvePathPreservingWindowsRoot(targetPath);
|
|
91
88
|
const root = resolvePathPreservingWindowsRoot(rootReal);
|
|
92
89
|
while (current !== root) {
|
|
93
90
|
try {
|
|
94
|
-
return captureDirectoryGuard(current,
|
|
91
|
+
return captureDirectoryGuard(current, "native", options);
|
|
95
92
|
}
|
|
96
93
|
catch (error) {
|
|
97
94
|
if (!isNotFoundPathError(error)) {
|
|
@@ -100,7 +97,7 @@ function nearestExistingDirectoryGuard(rootReal, targetPath, mode, options) {
|
|
|
100
97
|
current = path.dirname(current);
|
|
101
98
|
}
|
|
102
99
|
}
|
|
103
|
-
return captureDirectoryGuard(root,
|
|
100
|
+
return captureDirectoryGuard(root, "native", options);
|
|
104
101
|
}
|
|
105
102
|
// Recovery receipts must retain every identity bit, including on Windows.
|
|
106
103
|
export async function inspectDirectoryIdentity(dir, expected) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Exact caller observation. Unknown or rounded identities are not admitted. */
|
|
2
|
+
export type PublicationIdentity = Readonly<{
|
|
3
|
+
dev: bigint;
|
|
4
|
+
ino: bigint;
|
|
5
|
+
}>;
|
|
6
|
+
export type PublicationParent = Readonly<{
|
|
7
|
+
path: string;
|
|
8
|
+
identity: PublicationIdentity;
|
|
9
|
+
}>;
|
|
10
|
+
export type RetainEntryForPublicationOptions = Readonly<{
|
|
11
|
+
source: Readonly<{
|
|
12
|
+
parent: PublicationParent;
|
|
13
|
+
basename: string;
|
|
14
|
+
/** Windows symlink includes file/directory symbolic links and junctions; targets stay opaque. */
|
|
15
|
+
expected: PublicationIdentity & Readonly<{
|
|
16
|
+
kind: "directory" | "file" | "symlink";
|
|
17
|
+
}>;
|
|
18
|
+
}>;
|
|
19
|
+
destination: Readonly<{
|
|
20
|
+
parent: PublicationParent;
|
|
21
|
+
basename: string;
|
|
22
|
+
}>;
|
|
23
|
+
/** Must synchronously throw on loss of caller authority. Does not provide isolation. */
|
|
24
|
+
assertBeforeMutation: () => void;
|
|
25
|
+
}>;
|
|
26
|
+
export type EntryPublicationReceipt = Readonly<{
|
|
27
|
+
source: RetainEntryForPublicationOptions["source"];
|
|
28
|
+
destination: RetainEntryForPublicationOptions["destination"];
|
|
29
|
+
capability: Readonly<{
|
|
30
|
+
destinationAbsence: "atomic";
|
|
31
|
+
sourceIdentity: "observed-under-caller-exclusive-namespace";
|
|
32
|
+
parentBinding: "retained-object";
|
|
33
|
+
sourceFilesystem: string;
|
|
34
|
+
destinationFilesystem: string;
|
|
35
|
+
}>;
|
|
36
|
+
}>;
|
|
37
|
+
export type EntryPublicationIssue = Readonly<{
|
|
38
|
+
phase: "admission" | "authority" | "precheck" | "native" | "postcheck" | "close";
|
|
39
|
+
cause: unknown;
|
|
40
|
+
}>;
|
|
41
|
+
/** In-memory syscall disposition, NOT a crash-durable transaction journal. */
|
|
42
|
+
export type EntryPublicationResult = Readonly<{
|
|
43
|
+
transition: "committed" | "not-published" | "indeterminate";
|
|
44
|
+
verification: "not-performed" | "verified" | "failed";
|
|
45
|
+
resources: "closed" | "close-failed";
|
|
46
|
+
/** First issue is primary; subsequent close failures never replace it. */
|
|
47
|
+
issues: readonly EntryPublicationIssue[];
|
|
48
|
+
}>;
|
|
49
|
+
export interface RetainedEntryPublication extends Disposable {
|
|
50
|
+
readonly receipt: EntryPublicationReceipt;
|
|
51
|
+
/** One-shot, synchronous; returns operation AND close failures without discarding transition. */
|
|
52
|
+
publish(): EntryPublicationResult;
|
|
53
|
+
/** Close only. Never reverses, unlinks, or cleans either name; idempotent. */
|
|
54
|
+
dispose(): EntryPublicationResult;
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RetainedEntryPublication, RetainEntryForPublicationOptions } from "./entry-publication-types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Retain an existing directory, single-link regular file or symlink for ONE-WAY export.
|
|
4
|
+
* Requires caller-exclusive source namespace and stable admitted topology.
|
|
5
|
+
* Native destination absence is atomic; POSIX source identity is NOT CAS.
|
|
6
|
+
* All operation results settle descriptors, never delete or reverse names.
|
|
7
|
+
*/
|
|
8
|
+
export declare function retainEntryForPublication(options: RetainEntryForPublicationOptions): RetainedEntryPublication;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import fs, {} from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assertDirectoryIdentitySync } from "./directory-guard.js";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
6
|
+
import { captureNativeFdClose } from "./native-binding.js";
|
|
7
|
+
import { requireNativeBinding } from "./native.js";
|
|
8
|
+
import { realpathSync } from "./realpath.js";
|
|
9
|
+
import { describeStagedDirectory, assertStagedDirectoryCurrent } from "./staged-directory.js";
|
|
10
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
11
|
+
function basename(name) {
|
|
12
|
+
if (typeof name !== "string" || !name || name === "." || name === ".." ||
|
|
13
|
+
/[/\\:\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(name)) {
|
|
14
|
+
throw new FsSafeError("invalid-path", "publication requires a direct-child basename");
|
|
15
|
+
}
|
|
16
|
+
return name;
|
|
17
|
+
}
|
|
18
|
+
function identity(value) {
|
|
19
|
+
const { dev, ino } = value;
|
|
20
|
+
if (typeof dev !== "bigint" || typeof ino !== "bigint" || dev < 0n || ino <= 0n ||
|
|
21
|
+
dev > 0xffffffffffffffffn || ino > 0xffffffffffffffffn) {
|
|
22
|
+
throw new FsSafeError("path-mismatch", "publication requires an exact known identity");
|
|
23
|
+
}
|
|
24
|
+
return Object.freeze({ dev, ino });
|
|
25
|
+
}
|
|
26
|
+
function parent(value) {
|
|
27
|
+
const pathname = value.path;
|
|
28
|
+
const expected = identity(value.identity);
|
|
29
|
+
if (typeof pathname !== "string" || !path.isAbsolute(pathname) || path.resolve(pathname) !== pathname ||
|
|
30
|
+
realpathSync.native(pathname) !== pathname) {
|
|
31
|
+
throw new FsSafeError("path-alias", "publication parent must use its canonical physical spelling");
|
|
32
|
+
}
|
|
33
|
+
assertDirectoryIdentitySync(pathname, { ...expected, realPath: pathname });
|
|
34
|
+
return Object.freeze({ path: pathname, identity: expected });
|
|
35
|
+
}
|
|
36
|
+
function retainParent(expected, closes) {
|
|
37
|
+
const fd = fs.openSync(expected.path, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
38
|
+
// Register immediately: outer admission owns *all* close outcomes, including
|
|
39
|
+
// failures in the shared guard. A hidden admission close cannot report closed.
|
|
40
|
+
closes.push(() => fs.closeSync(fd));
|
|
41
|
+
const receipt = describeStagedDirectory(fd, expected.path);
|
|
42
|
+
inspectFileIdentitySync(() => fs.fstatSync(fd, { bigint: true }), expected.identity);
|
|
43
|
+
return { fd, receipt };
|
|
44
|
+
}
|
|
45
|
+
function assertKind(stat, kind) {
|
|
46
|
+
if (kind === "file" ? !stat.isFile() : kind === "symlink" ? !stat.isSymbolicLink() : !stat.isDirectory()) {
|
|
47
|
+
throw new FsSafeError("not-file", "publication source must be the expected file, directory or symlink");
|
|
48
|
+
}
|
|
49
|
+
if (kind !== "directory" && stat.nlink !== 1n) {
|
|
50
|
+
throw new FsSafeError("hardlink", "publication does not admit hardlinked files or symlinks");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function inspectEntry(name, expected) {
|
|
54
|
+
const stat = inspectFileIdentitySync(() => fs.lstatSync(name, { bigint: true }), expected);
|
|
55
|
+
assertKind(stat, expected.kind);
|
|
56
|
+
// A symlink target is opaque: it may be dangling, relative or inaccessible.
|
|
57
|
+
// Check the directory entry spelling without resolving or opening the target.
|
|
58
|
+
const canonical = expected.kind === "symlink"
|
|
59
|
+
? fs.readdirSync(path.dirname(name)).includes(path.basename(name))
|
|
60
|
+
: realpathSync.native(name) === name;
|
|
61
|
+
if (!canonical) {
|
|
62
|
+
throw new FsSafeError("path-alias", "publication source must use its canonical physical spelling");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function settle(closes, issues) {
|
|
66
|
+
let resources = "closed";
|
|
67
|
+
// Consume ownership before each close; a failed close must never be retried.
|
|
68
|
+
for (const close of closes.splice(0).reverse()) {
|
|
69
|
+
try {
|
|
70
|
+
for (const cause of close() ?? []) {
|
|
71
|
+
resources = "close-failed";
|
|
72
|
+
issues.push(Object.freeze({ phase: "close", cause }));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (cause) {
|
|
76
|
+
resources = "close-failed";
|
|
77
|
+
issues.push(Object.freeze({ phase: "close", cause }));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return resources;
|
|
81
|
+
}
|
|
82
|
+
class Publication {
|
|
83
|
+
#result;
|
|
84
|
+
#busy = false;
|
|
85
|
+
#receipt;
|
|
86
|
+
#backend;
|
|
87
|
+
#closes;
|
|
88
|
+
#assertion;
|
|
89
|
+
constructor(receipt, backend, closes, assertion) {
|
|
90
|
+
this.#receipt = receipt;
|
|
91
|
+
this.#backend = backend;
|
|
92
|
+
this.#closes = closes;
|
|
93
|
+
this.#assertion = assertion;
|
|
94
|
+
}
|
|
95
|
+
get receipt() { return this.#receipt; }
|
|
96
|
+
#idle() {
|
|
97
|
+
if (this.#busy)
|
|
98
|
+
throw new FsSafeError("helper-failed", "reentrant entry publication");
|
|
99
|
+
}
|
|
100
|
+
publish() {
|
|
101
|
+
this.#idle();
|
|
102
|
+
if (this.#result)
|
|
103
|
+
return this.#result;
|
|
104
|
+
this.#busy = true;
|
|
105
|
+
let transition = "not-published";
|
|
106
|
+
let verification = "not-performed";
|
|
107
|
+
let phase = "authority";
|
|
108
|
+
const issues = [];
|
|
109
|
+
try {
|
|
110
|
+
assertSynchronousCallbackResult(this.#assertion(), "assertBeforeMutation");
|
|
111
|
+
phase = "precheck";
|
|
112
|
+
this.#backend.current(false);
|
|
113
|
+
phase = "native";
|
|
114
|
+
// Any thrown/lost/malformed native reply is unknown, even if a later path
|
|
115
|
+
// observation happens to resemble success or failure.
|
|
116
|
+
transition = "indeterminate";
|
|
117
|
+
const native = this.#backend.publish();
|
|
118
|
+
if (native?.outcome !== "committed" && native?.outcome !== "not-published" && native?.outcome !== "indeterminate") {
|
|
119
|
+
throw new FsSafeError("helper-failed", "native publication returned an unknown outcome");
|
|
120
|
+
}
|
|
121
|
+
transition = native.outcome; // Capture BEFORE diagnostics, observations or close.
|
|
122
|
+
if (native.errorCode || transition !== "committed") {
|
|
123
|
+
throw Object.assign(new Error(native.errorMessage ?? "entry publication did not commit"), { code: native.errorCode ?? "helper-failed" });
|
|
124
|
+
}
|
|
125
|
+
phase = "postcheck";
|
|
126
|
+
verification = "failed";
|
|
127
|
+
this.#backend.current(true);
|
|
128
|
+
verification = "verified";
|
|
129
|
+
}
|
|
130
|
+
catch (cause) {
|
|
131
|
+
issues.push(Object.freeze({ phase, cause }));
|
|
132
|
+
}
|
|
133
|
+
const resources = settle(this.#closes, issues);
|
|
134
|
+
this.#result = Object.freeze({ transition, verification, resources, issues: Object.freeze(issues) });
|
|
135
|
+
this.#busy = false;
|
|
136
|
+
return this.#result;
|
|
137
|
+
}
|
|
138
|
+
dispose() {
|
|
139
|
+
this.#idle();
|
|
140
|
+
if (this.#result)
|
|
141
|
+
return this.#result;
|
|
142
|
+
this.#busy = true;
|
|
143
|
+
const issues = [];
|
|
144
|
+
const resources = settle(this.#closes, issues);
|
|
145
|
+
this.#result = Object.freeze({ transition: "not-published", verification: "not-performed",
|
|
146
|
+
resources, issues: Object.freeze(issues) });
|
|
147
|
+
this.#busy = false;
|
|
148
|
+
return this.#result;
|
|
149
|
+
}
|
|
150
|
+
[Symbol.dispose]() {
|
|
151
|
+
const result = this.dispose();
|
|
152
|
+
if (result.resources === "close-failed") {
|
|
153
|
+
throw new FsSafeError("helper-failed", "entry publication descriptor close failed", {
|
|
154
|
+
cause: result.issues[0]?.cause, details: { result },
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function publicationReceipt(source, destination, sourceFilesystem, destinationFilesystem) {
|
|
160
|
+
return Object.freeze({ source, destination, capability: Object.freeze({ destinationAbsence: "atomic",
|
|
161
|
+
sourceIdentity: "observed-under-caller-exclusive-namespace", parentBinding: "retained-object", sourceFilesystem, destinationFilesystem }) });
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Retain an existing directory, single-link regular file or symlink for ONE-WAY export.
|
|
165
|
+
* Requires caller-exclusive source namespace and stable admitted topology.
|
|
166
|
+
* Native destination absence is atomic; POSIX source identity is NOT CAS.
|
|
167
|
+
* All operation results settle descriptors, never delete or reverse names.
|
|
168
|
+
*/
|
|
169
|
+
export function retainEntryForPublication(options) {
|
|
170
|
+
const closes = [];
|
|
171
|
+
try {
|
|
172
|
+
if (process.platform !== "darwin" && process.platform !== "linux" && process.platform !== "win32") {
|
|
173
|
+
throw new FsSafeError("unsupported-platform", "entry publication requires macOS, Linux or Windows");
|
|
174
|
+
}
|
|
175
|
+
const native = requireNativeBinding();
|
|
176
|
+
const assertion = options.assertBeforeMutation;
|
|
177
|
+
if (typeof assertion !== "function")
|
|
178
|
+
throw new FsSafeError("invalid-path", "synchronous publication authority is required");
|
|
179
|
+
const kind = options.source.expected.kind;
|
|
180
|
+
if (kind !== "directory" && kind !== "file" && kind !== "symlink")
|
|
181
|
+
throw new FsSafeError("not-file", "unsupported publication entry kind");
|
|
182
|
+
const source = Object.freeze({ parent: parent(options.source.parent), basename: basename(options.source.basename),
|
|
183
|
+
expected: Object.freeze({ ...identity(options.source.expected), kind }) });
|
|
184
|
+
const destination = Object.freeze({ parent: parent(options.destination.parent), basename: basename(options.destination.basename) });
|
|
185
|
+
const sourcePath = path.join(source.parent.path, source.basename);
|
|
186
|
+
const targetPath = path.join(destination.parent.path, destination.basename);
|
|
187
|
+
if (targetPath === sourcePath || (kind === "directory" && targetPath.startsWith(`${sourcePath}${path.sep}`))) {
|
|
188
|
+
throw new FsSafeError("invalid-path", "publication source and destination overlap");
|
|
189
|
+
}
|
|
190
|
+
if (process.platform === "win32") {
|
|
191
|
+
if (typeof native.retainWindowsEntryPublication !== "function") {
|
|
192
|
+
throw new FsSafeError("helper-unavailable", "native Windows entry retention is unavailable");
|
|
193
|
+
}
|
|
194
|
+
inspectEntry(sourcePath, source.expected);
|
|
195
|
+
let owner;
|
|
196
|
+
// A lost admission reply cannot prove resource settlement. The native GC
|
|
197
|
+
// backstop closes only; an explicit unknown close must remain visible.
|
|
198
|
+
closes.push(() => {
|
|
199
|
+
if (!owner)
|
|
200
|
+
throw new FsSafeError("helper-failed", "native retention reply lost; closure unknown");
|
|
201
|
+
return owner.close().map(error => Object.assign(new Error(error.message), { code: error.code }));
|
|
202
|
+
});
|
|
203
|
+
owner = native.retainWindowsEntryPublication(source.parent.path, source.basename, source.parent.identity.dev, source.parent.identity.ino, destination.parent.path, destination.basename, destination.parent.identity.dev, destination.parent.identity.ino, source.expected.dev, source.expected.ino, kind);
|
|
204
|
+
const admitted = owner.admission;
|
|
205
|
+
if (admitted.outcome !== "retained" || admitted.errorCode) {
|
|
206
|
+
throw Object.assign(new Error(admitted.errorMessage ?? "native Windows publication admission failed"), { code: admitted.errorCode ?? "helper-failed" });
|
|
207
|
+
}
|
|
208
|
+
const retained = owner;
|
|
209
|
+
const receipt = publicationReceipt(source, destination, "ntfs", "ntfs");
|
|
210
|
+
return new Publication(receipt, { current: published => retained.current(published), publish: () => retained.publish() }, closes, assertion);
|
|
211
|
+
}
|
|
212
|
+
if (typeof native.entryPublicationFilesystem !== "function" ||
|
|
213
|
+
typeof native.publishRetainedEntryNoReplace !== "function" || typeof native.openBeneath !== "function") {
|
|
214
|
+
throw new FsSafeError("helper-unavailable", "native entry publication is unavailable");
|
|
215
|
+
}
|
|
216
|
+
const binding = native;
|
|
217
|
+
const closeSource = captureNativeFdClose(binding);
|
|
218
|
+
if (kind === "symlink" && typeof binding.openStagedSymlink !== "function") {
|
|
219
|
+
throw new FsSafeError("helper-unavailable", "native symlink retention is unavailable");
|
|
220
|
+
}
|
|
221
|
+
const sourceParent = retainParent(source.parent, closes);
|
|
222
|
+
const destinationParent = retainParent(destination.parent, closes);
|
|
223
|
+
const sourceFilesystem = binding.entryPublicationFilesystem(sourceParent.fd);
|
|
224
|
+
const destinationFilesystem = binding.entryPublicationFilesystem(destinationParent.fd);
|
|
225
|
+
if (source.expected.dev !== source.parent.identity.dev || source.expected.dev !== destination.parent.identity.dev) {
|
|
226
|
+
throw Object.assign(new Error("entry publication cannot cross devices"), { code: "EXDEV" });
|
|
227
|
+
}
|
|
228
|
+
inspectEntry(sourcePath, source.expected);
|
|
229
|
+
const sourceFd = kind === "symlink"
|
|
230
|
+
? binding.openStagedSymlink(sourceParent.fd, source.basename)
|
|
231
|
+
: binding.openBeneath(sourceParent.fd, source.basename, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK |
|
|
232
|
+
(kind === "directory" ? fs.constants.O_DIRECTORY : 0))?.fd;
|
|
233
|
+
if (!Number.isInteger(sourceFd) || sourceFd < 0) {
|
|
234
|
+
throw new FsSafeError("helper-unavailable", "publication source descriptor is unavailable");
|
|
235
|
+
}
|
|
236
|
+
closes.push(() => closeSource(sourceFd));
|
|
237
|
+
assertKind(inspectFileIdentitySync(() => fs.fstatSync(sourceFd, { bigint: true }), source.expected), kind);
|
|
238
|
+
inspectEntry(sourcePath, source.expected);
|
|
239
|
+
assertStagedDirectoryCurrent(sourceParent.receipt);
|
|
240
|
+
assertStagedDirectoryCurrent(destinationParent.receipt);
|
|
241
|
+
const receipt = publicationReceipt(source, destination, sourceFilesystem, destinationFilesystem);
|
|
242
|
+
return new Publication(receipt, {
|
|
243
|
+
current(published) {
|
|
244
|
+
assertStagedDirectoryCurrent(sourceParent.receipt);
|
|
245
|
+
assertStagedDirectoryCurrent(destinationParent.receipt);
|
|
246
|
+
assertKind(inspectFileIdentitySync(() => fs.fstatSync(sourceFd, { bigint: true }), source.expected), kind);
|
|
247
|
+
const location = published ? destination : source;
|
|
248
|
+
inspectEntry(path.join(location.parent.path, location.basename), source.expected);
|
|
249
|
+
},
|
|
250
|
+
publish: () => binding.publishRetainedEntryNoReplace(sourceParent.fd, source.basename, sourceFd, destinationParent.fd, destination.basename),
|
|
251
|
+
}, closes, assertion);
|
|
252
|
+
}
|
|
253
|
+
catch (cause) {
|
|
254
|
+
const issues = [Object.freeze({ phase: "admission", cause })];
|
|
255
|
+
const resources = settle(closes, issues);
|
|
256
|
+
const result = Object.freeze({ transition: "not-published", verification: "not-performed",
|
|
257
|
+
resources, issues: Object.freeze(issues) });
|
|
258
|
+
throw new FsSafeError(cause instanceof FsSafeError ? cause.code : "helper-failed", "entry publication admission failed", {
|
|
259
|
+
cause, details: { result },
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { hasNodeErrorCode } from "./path.js";
|
|
3
|
+
export function assertExclusiveCreateStat(pathname, stat) {
|
|
4
|
+
if (stat.isSymbolicLink()) {
|
|
5
|
+
throw Object.assign(new Error("exclusive creation target already exists"), {
|
|
6
|
+
code: "EEXIST", syscall: "open", path: pathname,
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
// Windows O_EXCL can create a dangling file symlink's referent. Keep the
|
|
11
|
+
// exclusive-open collision contract; this preflight cannot prevent a later swap.
|
|
12
|
+
export function assertExclusiveCreateLeaf(pathname) {
|
|
13
|
+
if (process.platform !== "win32")
|
|
14
|
+
return;
|
|
15
|
+
try {
|
|
16
|
+
assertExclusiveCreateStat(pathname, fs.lstatSync(pathname));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (!hasNodeErrorCode(error, "ENOENT"))
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { type BigIntStats } from "node:fs";
|
|
2
|
-
import
|
|
2
|
+
import { type FileHandle } from "node:fs/promises";
|
|
3
3
|
import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
4
4
|
export declare function readErrorCode(error: unknown): unknown;
|
|
5
5
|
export declare function hasErrorCode(error: unknown, expected: string): boolean;
|
|
6
6
|
type OwnedPathCleanupStatus = "removed" | "name-absent" | "preserved";
|
|
7
|
-
export declare function removeOwnedPath(params: {
|
|
8
|
-
fsModule: Pick<typeof fs, "lstat" | "unlink">;
|
|
9
|
-
pathname: string;
|
|
10
|
-
identity?: BigIntStats;
|
|
11
|
-
}): Promise<OwnedPathCleanupStatus>;
|
|
12
7
|
export declare function cleanupPinnedFilePath(params: {
|
|
13
8
|
pathname: string;
|
|
14
9
|
handle: Pick<FileHandle, "fd">;
|
|
@@ -17,20 +17,18 @@ export function readErrorCode(error) {
|
|
|
17
17
|
export function hasErrorCode(error, expected) {
|
|
18
18
|
return readErrorCode(error) === expected;
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
async function removeOwnedPath(params) {
|
|
21
21
|
if (!params.identity)
|
|
22
22
|
return "preserved";
|
|
23
23
|
try {
|
|
24
|
-
const current = params.
|
|
25
|
-
? syncFs.lstatSync(params.pathname, { bigint: true })
|
|
26
|
-
: await params.fsModule.lstat(params.pathname, { bigint: true });
|
|
24
|
+
const current = syncFs.lstatSync(params.pathname, { bigint: true });
|
|
27
25
|
if (current.isSymbolicLink() ||
|
|
28
26
|
!current.isFile() ||
|
|
29
27
|
current.nlink !== 1n ||
|
|
30
28
|
!sameFileIdentityForCleanup(current, params.identity)) {
|
|
31
29
|
return "preserved";
|
|
32
30
|
}
|
|
33
|
-
await
|
|
31
|
+
await fs.unlink(params.pathname);
|
|
34
32
|
return "removed";
|
|
35
33
|
}
|
|
36
34
|
catch (error) {
|
|
@@ -68,7 +66,6 @@ export async function cleanupPinnedFilePath(params) {
|
|
|
68
66
|
}
|
|
69
67
|
try {
|
|
70
68
|
return await removeOwnedPath({
|
|
71
|
-
fsModule: fs,
|
|
72
69
|
pathname: params.pathname,
|
|
73
70
|
identity: params.identity,
|
|
74
71
|
});
|
|
@@ -34,12 +34,9 @@ export function sameFileIdentityForCleanup(left, right, platform = process.platf
|
|
|
34
34
|
if (platform !== "win32") {
|
|
35
35
|
return sameCleanupStatValue(left.dev, right.dev) && sameCleanupStatValue(left.ino, right.ino);
|
|
36
36
|
}
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// Capture each potentially adapter-backed observation once. This strengthens
|
|
41
|
-
// the fence against changing accessors; exact legacy getter traces are not a
|
|
42
|
-
// compatibility contract.
|
|
37
|
+
// Unknown Windows identity components cannot authorize cleanup: a replacement
|
|
38
|
+
// could share the known component. Snapshot adapter-backed fields once so
|
|
39
|
+
// changing accessors cannot alter the check.
|
|
43
40
|
const leftDev = left.dev;
|
|
44
41
|
if (isZero(leftDev))
|
|
45
42
|
return false;
|
|
@@ -4,6 +4,7 @@ import { readFileDescriptorBoundedSync } from "./bounded-read.js";
|
|
|
4
4
|
import { inspectDirectoryIdentitySync } from "./directory-guard.js";
|
|
5
5
|
import { FsSafeError } from "./errors.js";
|
|
6
6
|
import { isNotFoundPathError } from "./path.js";
|
|
7
|
+
import { sameAbsolutePath } from "./path-segment-route.js";
|
|
7
8
|
import { assertRootIdentityCurrentSync } from "./root-context.js";
|
|
8
9
|
import { hardlinkedPathNotAllowedError } from "./root-errors.js";
|
|
9
10
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
@@ -14,12 +15,11 @@ import { createSuppressedError } from "./suppressed-error.js";
|
|
|
14
15
|
import { assertFileLockSyncRootResolvedPathCurrent, } from "./file-lock-sync-root.js";
|
|
15
16
|
import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-path-alias.js";
|
|
16
17
|
const MAX_SIDECAR_LOCK_PAYLOAD_BYTES = 1024 * 1024;
|
|
17
|
-
const samePath = (left, right) => path.relative(path.resolve(left), path.resolve(right)) === "";
|
|
18
18
|
export function observeDirectory(pathname, initial) {
|
|
19
19
|
const stat = inspectDirectoryIdentitySync(pathname, undefined, initial);
|
|
20
20
|
const realPath = realpathSync.native(pathForWindowsFilesystem(pathname));
|
|
21
21
|
assertNoWindowsPathAlias(realPath, "filesystem", "sidecar lock parent uses a Windows filesystem namespace alias");
|
|
22
|
-
if (!
|
|
22
|
+
if (!sameAbsolutePath(realPath, pathname)) {
|
|
23
23
|
throw new FsSafeError("path-mismatch", "sidecar lock parent changed during operation");
|
|
24
24
|
}
|
|
25
25
|
return Object.freeze({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs, {} from "node:fs";
|
|
2
|
+
import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { inspectDirectoryIdentitySync } from "./directory-guard.js";
|
|
4
5
|
import { FsSafeError } from "./errors.js";
|
|
@@ -89,6 +90,14 @@ export function createFileLockSyncRootFile(pathAuthority, options) {
|
|
|
89
90
|
let receipt;
|
|
90
91
|
try {
|
|
91
92
|
options.assertBeforeOpen();
|
|
93
|
+
try {
|
|
94
|
+
assertExclusiveCreateLeaf(pathForWindowsFilesystem(pathAuthority.path));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error.code === "EEXIST")
|
|
98
|
+
options.onOpenFailure(error);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
92
101
|
try {
|
|
93
102
|
fd = fs.openSync(pathForWindowsFilesystem(pathAuthority.path), fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow, 0o600);
|
|
94
103
|
}
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { assertMutationNotDenied } from "./deny-mutations.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { assertNoNulPathInput, isNotFoundPathError } from "./path.js";
|
|
6
|
+
import { sameAbsolutePath } from "./path-segment-route.js";
|
|
6
7
|
import { resolveRootPathSync } from "./root-path.js";
|
|
7
8
|
import { isRootPathEscapeError } from "./root-path-errors.js";
|
|
8
9
|
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
@@ -91,9 +92,6 @@ export function invokeFileLockSyncRootMutationAuthority(authority) {
|
|
|
91
92
|
assertSynchronousCallbackResult(assertion(), "assertBeforeMutation");
|
|
92
93
|
return true;
|
|
93
94
|
}
|
|
94
|
-
function samePath(left, right) {
|
|
95
|
-
return path.relative(path.resolve(left), path.resolve(right)) === "";
|
|
96
|
-
}
|
|
97
95
|
function absoluteRootPath(authority, relativePath) {
|
|
98
96
|
const segments = relativePath.split(path.posix.sep);
|
|
99
97
|
const absolute = path.join(authority.context.rootReal, ...segments);
|
|
@@ -129,7 +127,7 @@ function resolveAdmittedPath(authority, absolutePath, policy, rootPath) {
|
|
|
129
127
|
candidatePath: resolved.canonicalPath,
|
|
130
128
|
rootIdentity: context.rootIdentity,
|
|
131
129
|
});
|
|
132
|
-
if (!admitted || admitted.relativePath === "" ||
|
|
130
|
+
if (!admitted || admitted.relativePath === "" || sameAbsolutePath(admitted.path, context.rootReal)) {
|
|
133
131
|
throw outsideWorkspaceError();
|
|
134
132
|
}
|
|
135
133
|
return {
|
|
@@ -143,7 +141,7 @@ function resolveBothPolicies(authority, absolutePath, rootPath) {
|
|
|
143
141
|
const readable = policiesMatch
|
|
144
142
|
? mutation
|
|
145
143
|
: resolveAdmittedPath(authority, absolutePath, readPolicy, rootPath);
|
|
146
|
-
if (!
|
|
144
|
+
if (!sameAbsolutePath(mutation.path, readable.path)) {
|
|
147
145
|
throw new FsSafeError("path-mismatch", "sidecar read and mutation paths resolve differently");
|
|
148
146
|
}
|
|
149
147
|
assertFileLockSyncRootMutationAllowed(mutation.path, authority.denyMutations);
|
|
@@ -190,7 +188,7 @@ export function admitFileLockSyncRootPath(authority, requestedPath) {
|
|
|
190
188
|
}
|
|
191
189
|
export function assertFileLockSyncRootResolvedPathCurrent(pathAuthority) {
|
|
192
190
|
const resolved = resolveBothPolicies(pathAuthority.authority, absoluteRootPath(pathAuthority.authority, pathAuthority.relativePath), pathAuthority.authority.context.rootReal);
|
|
193
|
-
if (!
|
|
191
|
+
if (!sameAbsolutePath(resolved.path, pathAuthority.path)) {
|
|
194
192
|
throw new FsSafeError("path-mismatch", "sidecar lock path changed during operation");
|
|
195
193
|
}
|
|
196
194
|
}
|