@openclaw/fs-safe 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/README.md +38 -10
- package/dist/advanced.d.ts +2 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +2 -1
- package/dist/archive-kind.d.ts +0 -1
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +5 -17
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +113 -46
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +10 -11
- package/dist/archive-tar-stream.d.ts +3 -0
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +56 -37
- package/dist/archive-tar-wasm.d.ts +16 -4
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +134 -34
- package/dist/archive-zip-directory.d.ts +4 -0
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +2 -0
- package/dist/archive-zip-entry.d.ts +6 -2
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-entry.js +23 -8
- package/dist/archive-zip-integrity.d.ts.map +1 -1
- package/dist/archive-zip-integrity.js +3 -4
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +107 -31
- package/dist/archive-zip-names.d.ts +1 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +6 -0
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +11 -11
- package/dist/bounded-read-stream.d.ts +0 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +0 -6
- package/dist/clone-metadata.d.ts +1 -0
- package/dist/clone-metadata.d.ts.map +1 -1
- package/dist/clone-metadata.js +6 -2
- package/dist/copy-publication.d.ts +6 -0
- package/dist/copy-publication.d.ts.map +1 -1
- package/dist/copy-publication.js +3 -0
- package/dist/copy-tree-portable.d.ts.map +1 -1
- package/dist/copy-tree-portable.js +44 -24
- package/dist/copy.d.ts.map +1 -1
- package/dist/copy.js +29 -11
- package/dist/create-directory.d.ts +20 -0
- package/dist/create-directory.d.ts.map +1 -0
- package/dist/create-directory.js +130 -0
- package/dist/create-file-async.d.ts +7 -0
- package/dist/create-file-async.d.ts.map +1 -0
- package/dist/create-file-async.js +121 -0
- package/dist/create-file.d.ts +8 -0
- package/dist/create-file.d.ts.map +1 -0
- package/dist/create-file.js +190 -0
- package/dist/create-owned-file.d.ts +8 -0
- package/dist/create-owned-file.d.ts.map +1 -0
- package/dist/create-owned-file.js +16 -0
- package/dist/create.d.ts +4 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +2 -0
- package/dist/creation-darwin.d.ts +7 -0
- package/dist/creation-darwin.d.ts.map +1 -0
- package/dist/creation-darwin.js +79 -0
- package/dist/creation-file-state.d.ts +19 -0
- package/dist/creation-file-state.d.ts.map +1 -0
- package/dist/creation-file-state.js +118 -0
- package/dist/creation-path.d.ts +21 -0
- package/dist/creation-path.d.ts.map +1 -0
- package/dist/creation-path.js +71 -0
- package/dist/creation-permissions.d.ts +19 -0
- package/dist/creation-permissions.d.ts.map +1 -0
- package/dist/creation-permissions.js +125 -0
- package/dist/directory-durability.d.ts +1 -1
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +22 -80
- package/dist/directory-guard.d.ts +3 -0
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-mode-node.d.ts +2 -0
- package/dist/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +8 -0
- package/dist/directory-mode-owner.js +5 -5
- package/dist/directory-receipt.d.ts +24 -0
- package/dist/directory-receipt.d.ts.map +1 -0
- package/dist/directory-receipt.js +127 -0
- package/dist/file-cleanup.d.ts +19 -0
- package/dist/file-cleanup.d.ts.map +1 -0
- package/dist/file-cleanup.js +78 -0
- package/dist/file-handle-transfer.d.ts +2 -0
- package/dist/file-handle-transfer.d.ts.map +1 -1
- package/dist/file-handle-transfer.js +57 -2
- package/dist/file-identity.d.ts.map +1 -1
- package/dist/file-identity.js +18 -4
- package/dist/file-lock-sync-admission.d.ts +19 -0
- package/dist/file-lock-sync-admission.d.ts.map +1 -0
- package/dist/file-lock-sync-admission.js +93 -0
- package/dist/file-lock-sync-root-acquire.d.ts +4 -0
- package/dist/file-lock-sync-root-acquire.d.ts.map +1 -0
- package/dist/file-lock-sync-root-acquire.js +370 -0
- package/dist/file-lock-sync-root-arbitration.d.ts +18 -0
- package/dist/file-lock-sync-root-arbitration.d.ts.map +1 -0
- package/dist/file-lock-sync-root-arbitration.js +66 -0
- package/dist/file-lock-sync-root-held.d.ts +34 -0
- package/dist/file-lock-sync-root-held.d.ts.map +1 -0
- package/dist/file-lock-sync-root-held.js +393 -0
- package/dist/file-lock-sync-root-io.d.ts +44 -0
- package/dist/file-lock-sync-root-io.d.ts.map +1 -0
- package/dist/file-lock-sync-root-io.js +209 -0
- package/dist/file-lock-sync-root-mutation.d.ts +17 -0
- package/dist/file-lock-sync-root-mutation.d.ts.map +1 -0
- package/dist/file-lock-sync-root-mutation.js +277 -0
- package/dist/file-lock-sync-root-options.d.ts +20 -0
- package/dist/file-lock-sync-root-options.d.ts.map +1 -0
- package/dist/file-lock-sync-root-options.js +58 -0
- package/dist/file-lock-sync-root-registration.d.ts +2 -0
- package/dist/file-lock-sync-root-registration.d.ts.map +1 -0
- package/dist/file-lock-sync-root-registration.js +90 -0
- package/dist/file-lock-sync-root.d.ts +36 -0
- package/dist/file-lock-sync-root.d.ts.map +1 -0
- package/dist/file-lock-sync-root.js +361 -0
- package/dist/file-lock-sync-stale-admission.d.ts +24 -0
- package/dist/file-lock-sync-stale-admission.d.ts.map +1 -0
- package/dist/file-lock-sync-stale-admission.js +205 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +245 -205
- package/dist/file-observation.d.ts +1 -1
- package/dist/file-observation.d.ts.map +1 -1
- package/dist/file-store-boundary.d.ts +2 -6
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +3 -9
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +5 -1
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +5 -8
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +47 -12
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +36 -7
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +22 -15
- package/dist/json-durable-queue-ownership.d.ts +0 -1
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +0 -6
- package/dist/move-path.js +1 -1
- package/dist/native-binding.d.ts +13 -1
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-fallback-warning.d.ts +4 -0
- package/dist/native-fallback-warning.d.ts.map +1 -0
- package/dist/native-fallback-warning.js +11 -0
- package/dist/native-operations.d.ts +0 -2
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +0 -24
- package/dist/native-parent-admission.d.ts +5 -2
- package/dist/native-parent-admission.d.ts.map +1 -1
- package/dist/native-parent-admission.js +27 -7
- package/dist/native-pinned-write-windows.d.ts +1 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +174 -29
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +26 -14
- package/dist/native-policy-parent-windows.d.ts +14 -0
- package/dist/native-policy-parent-windows.d.ts.map +1 -0
- package/dist/native-policy-parent-windows.js +209 -0
- package/dist/native-rename-outcome.d.ts +4 -0
- package/dist/native-rename-outcome.d.ts.map +1 -0
- package/dist/native-rename-outcome.js +8 -0
- package/dist/native-staged-file.d.ts +3 -2
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +121 -72
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +12 -8
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +10 -4
- package/dist/path-prefix.d.ts.map +1 -1
- package/dist/path-prefix.js +30 -8
- package/dist/path-suffix-aliases.d.ts +2 -0
- package/dist/path-suffix-aliases.d.ts.map +1 -1
- package/dist/path-suffix-aliases.js +25 -17
- package/dist/permission-exec.d.ts +2 -0
- package/dist/permission-exec.d.ts.map +1 -1
- package/dist/permission-exec.js +150 -21
- package/dist/permissions-windows.js +1 -1
- package/dist/pinned-mutation-admission.d.ts.map +1 -1
- package/dist/pinned-mutation-admission.js +10 -5
- package/dist/pinned-mutation-observation.d.ts +0 -1
- package/dist/pinned-mutation-observation.d.ts.map +1 -1
- package/dist/pinned-mutation-observation.js +0 -19
- package/dist/pinned-mutation-shared-route.d.ts +1 -0
- package/dist/pinned-mutation-shared-route.d.ts.map +1 -1
- package/dist/pinned-mutation-shared-route.js +1 -1
- package/dist/pinned-write-input.d.ts +4 -0
- package/dist/pinned-write-input.d.ts.map +1 -0
- package/dist/pinned-write-input.js +25 -0
- package/dist/pinned-write-mode.d.ts +5 -0
- package/dist/pinned-write-mode.d.ts.map +1 -0
- package/dist/pinned-write-mode.js +24 -0
- package/dist/pinned-write-staged.d.ts +6 -0
- package/dist/pinned-write-staged.d.ts.map +1 -0
- package/dist/pinned-write-staged.js +187 -0
- package/dist/pinned-write-types.d.ts +5 -0
- package/dist/pinned-write-types.d.ts.map +1 -1
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +35 -145
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +18 -4
- package/dist/private-producer-handoff-sync.d.ts +14 -0
- package/dist/private-producer-handoff-sync.d.ts.map +1 -0
- package/dist/private-producer-handoff-sync.js +114 -0
- package/dist/private-producer-handoff.d.ts +22 -4
- package/dist/private-producer-handoff.d.ts.map +1 -1
- package/dist/private-producer-handoff.js +140 -77
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +75 -121
- package/dist/publish-copy-stage.d.ts +2 -1
- package/dist/publish-copy-stage.d.ts.map +1 -1
- package/dist/publish-copy-stage.js +16 -7
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -2
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +1 -15
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +256 -18
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +62 -70
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +10 -12
- package/dist/replace-file-temp-owner.d.ts +5 -9
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +56 -72
- package/dist/replace-file.js +6 -6
- package/dist/retained-directory-replacement.d.ts +26 -0
- package/dist/retained-directory-replacement.d.ts.map +1 -0
- package/dist/retained-directory-replacement.js +193 -0
- package/dist/root-boundary.d.ts +1 -0
- package/dist/root-boundary.d.ts.map +1 -1
- package/dist/root-boundary.js +4 -0
- package/dist/root-context.d.ts +0 -8
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +0 -3
- package/dist/root-create-input.d.ts +8 -1
- package/dist/root-create-input.d.ts.map +1 -1
- package/dist/root-create-input.js +17 -4
- package/dist/root-directory-creation.d.ts +3 -3
- package/dist/root-directory-creation.d.ts.map +1 -1
- package/dist/root-directory-creation.js +15 -3
- package/dist/root-directory-list.d.ts +1 -0
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +1 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +46 -17
- package/dist/root-move-noreplace.d.ts.map +1 -1
- package/dist/root-move-noreplace.js +24 -15
- package/dist/root-options.d.ts +12 -4
- package/dist/root-options.d.ts.map +1 -1
- package/dist/root-path-errors.d.ts +1 -0
- package/dist/root-path-errors.d.ts.map +1 -1
- package/dist/root-path-errors.js +11 -2
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +11 -35
- package/dist/root-path-stat.d.ts.map +1 -1
- package/dist/root-path-stat.js +59 -7
- package/dist/root-path.js +1 -13
- package/dist/root-remove.d.ts +1 -0
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +4 -0
- package/dist/root-walk.d.ts +1 -1
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +17 -2
- package/dist/root-write-admission.d.ts +0 -2
- package/dist/root-write-admission.d.ts.map +1 -1
- package/dist/root-write-admission.js +1 -15
- package/dist/root-write-complete-parent.d.ts.map +1 -1
- package/dist/root-write-complete-parent.js +7 -23
- package/dist/root-write-publication.js +1 -1
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +29 -42
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +3 -24
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +3 -24
- package/dist/secret-read-policy.d.ts +6 -2
- package/dist/secret-read-policy.d.ts.map +1 -1
- package/dist/secret-read-policy.js +26 -2
- package/dist/secure-file-windows.d.ts +6 -0
- package/dist/secure-file-windows.d.ts.map +1 -1
- package/dist/secure-file-windows.js +34 -117
- package/dist/secure-file.js +2 -2
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +23 -12
- package/dist/sidecar-lock-acquire.d.ts +2 -28
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +288 -199
- package/dist/sidecar-lock-admission-context.d.ts +19 -0
- package/dist/sidecar-lock-admission-context.d.ts.map +1 -0
- package/dist/sidecar-lock-admission-context.js +60 -0
- package/dist/sidecar-lock-admission-parser.d.ts +43 -0
- package/dist/sidecar-lock-admission-parser.d.ts.map +1 -0
- package/dist/sidecar-lock-admission-parser.js +113 -0
- package/dist/sidecar-lock-admission.d.ts +35 -0
- package/dist/sidecar-lock-admission.d.ts.map +1 -0
- package/dist/sidecar-lock-admission.js +7 -0
- package/dist/sidecar-lock-reclaim.d.ts +9 -4
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +80 -25
- package/dist/sidecar-lock-root.d.ts.map +1 -1
- package/dist/sidecar-lock-root.js +2 -1
- package/dist/sidecar-lock-stale-admission.d.ts +39 -0
- package/dist/sidecar-lock-stale-admission.d.ts.map +1 -0
- package/dist/sidecar-lock-stale-admission.js +232 -0
- package/dist/sidecar-lock-target.d.ts +8 -0
- package/dist/sidecar-lock-target.d.ts.map +1 -0
- package/dist/sidecar-lock-target.js +55 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +100 -16
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +6 -6
- package/dist/staged-file-settlement.d.ts +17 -0
- package/dist/staged-file-settlement.d.ts.map +1 -0
- package/dist/staged-file-settlement.js +57 -0
- package/dist/temp-workspace-descriptor.d.ts.map +1 -1
- package/dist/temp-workspace-descriptor.js +9 -27
- package/dist/temp-workspace-owner.d.ts.map +1 -1
- package/dist/temp-workspace-owner.js +8 -8
- package/dist/walk.d.ts +5 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +19 -6
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +4 -3
- package/dist/windows-security-bridge.cs +336 -0
- package/dist/windows-security-bridge.ps1 +15 -0
- package/dist/windows-security-command.d.ts +26 -0
- package/dist/windows-security-command.d.ts.map +1 -0
- package/dist/windows-security-command.js +363 -0
- package/dist/windows-security-facts.d.ts +6 -0
- package/dist/windows-security-facts.d.ts.map +1 -0
- package/dist/windows-security-facts.js +108 -0
- package/docs/advanced.md +4 -2
- package/docs/archive.md +97 -46
- package/docs/atomic.md +85 -8
- package/docs/config.md +6 -2
- package/docs/contributing.md +44 -4
- package/docs/copy.md +37 -0
- package/docs/creation.md +128 -0
- package/docs/durability.md +24 -0
- package/docs/file-store.md +19 -0
- package/docs/install.md +31 -7
- package/docs/json-store.md +5 -0
- package/docs/migrating-to-0.5.md +15 -6
- package/docs/migrating-to-0.6.md +9 -4
- package/docs/native-helper.md +32 -12
- package/docs/native.md +38 -7
- package/docs/output.md +6 -0
- package/docs/path-prefix.md +10 -0
- package/docs/path-suffix-aliases.md +51 -6
- package/docs/permissions.md +50 -14
- package/docs/public-api.md +3 -2
- package/docs/root.md +56 -3
- package/docs/secret-file.md +11 -2
- package/docs/secure-file.md +9 -4
- package/docs/sidecar-lock.md +114 -8
- package/docs/staged-file.md +12 -3
- package/docs/temp.md +20 -3
- package/docs/walk.md +67 -1
- package/docs/writing.md +76 -6
- package/package.json +19 -16
- package/dist/darwin-acl.d.ts +0 -4
- package/dist/darwin-acl.d.ts.map +0 -1
- package/dist/darwin-acl.js +0 -24
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { DEFAULT_PERMISSION_EXEC_TIMEOUT_MS, PermissionCommandError } from "./permission-exec.js";
|
|
5
|
+
import { resolveWindowsSystemCommand } from "./windows-command.js";
|
|
6
|
+
import { parseWindowsSecurityCommandFacts } from "./windows-security-facts.js";
|
|
7
|
+
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
8
|
+
const TERMINATION_GRACE_MS = 1_000;
|
|
9
|
+
const FULL_IDENTITY = /^[0-9a-f]{16}:[0-9a-f]{32}$/;
|
|
10
|
+
function isFullIdentity(identity) {
|
|
11
|
+
return typeof identity === "string" && identity.length === 49 && FULL_IDENTITY.test(identity);
|
|
12
|
+
}
|
|
13
|
+
function assertIdentity(identity) {
|
|
14
|
+
if (!isFullIdentity(identity)) {
|
|
15
|
+
throw new TypeError("Windows security operation requires a complete file identity");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function command(operation, params) {
|
|
19
|
+
const targetPath = params.targetPath ?? "";
|
|
20
|
+
// Preserve native EINVAL before child-process environment validation runs.
|
|
21
|
+
if (targetPath.includes("\0")) {
|
|
22
|
+
throw Object.assign(new Error("Windows path contains a NUL byte"), { code: "EINVAL" });
|
|
23
|
+
}
|
|
24
|
+
if (params.fd !== undefined || operation === "descriptor" || operation === "protect-file" || operation === "verify-file") {
|
|
25
|
+
if (typeof params.fd !== "number" || !Number.isInteger(params.fd) || params.fd < 0 || params.fd > 0x7fff_ffff) {
|
|
26
|
+
unverified("Windows security inspection requires a valid descriptor");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (params.expectedParentIdentity !== undefined)
|
|
30
|
+
assertIdentity(params.expectedParentIdentity);
|
|
31
|
+
if (params.expectedFileIdentity !== undefined)
|
|
32
|
+
assertIdentity(params.expectedFileIdentity);
|
|
33
|
+
if (params.expectedLinks !== undefined &&
|
|
34
|
+
(!Number.isInteger(params.expectedLinks) || params.expectedLinks < 1 || params.expectedLinks > 0xffff_ffff)) {
|
|
35
|
+
throw new TypeError("Windows security operation requires a positive uint32 link count");
|
|
36
|
+
}
|
|
37
|
+
const script = fileURLToPath(new URL("./windows-security-bridge.ps1", import.meta.url));
|
|
38
|
+
return {
|
|
39
|
+
file: resolveWindowsSystemCommand(String.raw `WindowsPowerShell\v1.0\powershell.exe`),
|
|
40
|
+
args: ["-NoLogo", "-NoProfile", "-NonInteractive", "-File", script, "-Operation", operation],
|
|
41
|
+
// Preserve Windows UTF-16 spelling and leave long paths out of the bounded
|
|
42
|
+
// command line. This overrides any inherited value only in this child.
|
|
43
|
+
env: {
|
|
44
|
+
...process.env,
|
|
45
|
+
FS_SAFE_WINDOWS_SECURITY_PATH: targetPath,
|
|
46
|
+
FS_SAFE_WINDOWS_SECURITY_REQUIRE_PRIVATE: params.requirePrivate ? "1" : "0",
|
|
47
|
+
FS_SAFE_WINDOWS_SECURITY_PARENT_IDENTITY: params.expectedParentIdentity ?? "",
|
|
48
|
+
FS_SAFE_WINDOWS_SECURITY_FILE_IDENTITY: params.expectedFileIdentity ?? "",
|
|
49
|
+
FS_SAFE_WINDOWS_SECURITY_EXPECTED_LINKS: String(params.expectedLinks ?? 1),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function unverified(message, cause) {
|
|
54
|
+
throw new FsSafeError("permission-unverified", message, cause === undefined ? {} : { cause });
|
|
55
|
+
}
|
|
56
|
+
function record(value) {
|
|
57
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
function parseReply(stdout, operation) {
|
|
60
|
+
try {
|
|
61
|
+
return parseReplyValue(stdout, operation);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (operation === "create" && error instanceof Error && error.code !== "EEXIST") {
|
|
65
|
+
Object.assign(error, { creationOutcome: "unconfirmed" });
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function parseReplyValue(stdout, operation) {
|
|
71
|
+
let response;
|
|
72
|
+
try {
|
|
73
|
+
response = JSON.parse(stdout.trim());
|
|
74
|
+
}
|
|
75
|
+
catch (cause) {
|
|
76
|
+
unverified("Windows security command returned invalid data", cause);
|
|
77
|
+
}
|
|
78
|
+
if (!record(response) || typeof response.ok !== "boolean") {
|
|
79
|
+
unverified("Windows security command returned an incomplete response");
|
|
80
|
+
}
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const codes = new Set(["EACCES", "EPERM", "EEXIST", "ENOENT", "ENOTSUP", "EIO", "EBADF", "ELOOP", "ENOTDIR", "EINVAL", "ENOSPC", "EBUSY"]);
|
|
83
|
+
if (typeof response.code !== "string" || !codes.has(response.code) || typeof response.message !== "string") {
|
|
84
|
+
unverified("Windows security command returned an invalid failure");
|
|
85
|
+
}
|
|
86
|
+
throw Object.assign(new Error(response.message), { code: response.code });
|
|
87
|
+
}
|
|
88
|
+
if (operation === "create" && (!record(response.result) || response.result.created !== true || !isFullIdentity(response.result.identity))) {
|
|
89
|
+
unverified("Windows security command did not verify private-directory creation");
|
|
90
|
+
}
|
|
91
|
+
return response.result;
|
|
92
|
+
}
|
|
93
|
+
function executeSync(operation, params) {
|
|
94
|
+
const { file, args, env } = command(operation, params);
|
|
95
|
+
const startedAt = performance.now();
|
|
96
|
+
const result = spawnSync(file, args, {
|
|
97
|
+
encoding: "utf8", windowsHide: true, env, stdio: [params.fd ?? "ignore", "pipe", "pipe"],
|
|
98
|
+
timeout: DEFAULT_PERMISSION_EXEC_TIMEOUT_MS, killSignal: "SIGKILL", maxBuffer: MAX_OUTPUT_BYTES,
|
|
99
|
+
});
|
|
100
|
+
if (result.error || result.status !== 0) {
|
|
101
|
+
const error = new PermissionCommandError(file, performance.now() - startedAt, {
|
|
102
|
+
cause: result.error, code: result.status,
|
|
103
|
+
signal: result.error?.code === "ETIMEDOUT" ? "SIGKILL" : result.signal,
|
|
104
|
+
stderr: result.stderr,
|
|
105
|
+
killed: result.error?.code === "ETIMEDOUT",
|
|
106
|
+
...(operation === "create" ? { creationOutcome: "unconfirmed" } : {}),
|
|
107
|
+
});
|
|
108
|
+
if (operation === "create")
|
|
109
|
+
Object.assign(error, { creationOutcome: "unconfirmed" });
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
return parseReply(result.stdout, operation);
|
|
113
|
+
}
|
|
114
|
+
class WindowsSecurityCommandError extends PermissionCommandError {
|
|
115
|
+
timedOut;
|
|
116
|
+
creationOutcome;
|
|
117
|
+
processExitConfirmed;
|
|
118
|
+
constructor(file, durationMs, receipt, timedOut) {
|
|
119
|
+
super(file, durationMs, receipt);
|
|
120
|
+
this.timedOut = timedOut;
|
|
121
|
+
this.creationOutcome = receipt.creationOutcome;
|
|
122
|
+
this.processExitConfirmed = receipt.processExitConfirmed;
|
|
123
|
+
if (timedOut)
|
|
124
|
+
this.message = `Windows permission inspection timed out after ${DEFAULT_PERMISSION_EXEC_TIMEOUT_MS}ms`;
|
|
125
|
+
if (!receipt.processExitConfirmed)
|
|
126
|
+
this.message += "; process exit was not confirmed";
|
|
127
|
+
else if (!receipt.outputClosed)
|
|
128
|
+
this.message += "; command output did not close";
|
|
129
|
+
if (receipt.creationOutcome)
|
|
130
|
+
this.message += "; private-directory creation outcome is unconfirmed";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Cleanup must preserve stages still reachable by an unsettled command. */
|
|
134
|
+
export function hasUnsettledWindowsSecurityCommand(error) {
|
|
135
|
+
const pending = [error];
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
for (let inspected = 0; pending.length && inspected < 32; inspected++) {
|
|
138
|
+
const current = pending.pop();
|
|
139
|
+
if (current === null || typeof current !== "object" || seen.has(current))
|
|
140
|
+
continue;
|
|
141
|
+
seen.add(current);
|
|
142
|
+
try {
|
|
143
|
+
if (current instanceof WindowsSecurityCommandError && !current.processExitConfirmed)
|
|
144
|
+
return true;
|
|
145
|
+
const cause = Object.getOwnPropertyDescriptor(current, "cause");
|
|
146
|
+
if (cause && "value" in cause)
|
|
147
|
+
pending.push(cause.value);
|
|
148
|
+
if (current instanceof AggregateError) {
|
|
149
|
+
const errors = Object.getOwnPropertyDescriptor(current, "errors");
|
|
150
|
+
if (errors && "value" in errors && Array.isArray(errors.value))
|
|
151
|
+
pending.push(...errors.value.slice(0, 32));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Unknown error wrappers cannot prove that command-owned handles settled.
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return pending.length > 0;
|
|
160
|
+
}
|
|
161
|
+
function ignoreLateError() { }
|
|
162
|
+
async function execute(operation, params) {
|
|
163
|
+
const { file, args, env } = command(operation, params);
|
|
164
|
+
const startedAt = performance.now();
|
|
165
|
+
return await new Promise((resolve, reject) => {
|
|
166
|
+
const child = spawn(file, args, { windowsHide: true, env, stdio: [params.fd ?? "ignore", "pipe", "pipe"] });
|
|
167
|
+
const output = [];
|
|
168
|
+
const errors = [];
|
|
169
|
+
let bytes = 0;
|
|
170
|
+
let failure;
|
|
171
|
+
let failed = false;
|
|
172
|
+
let timedOut = false;
|
|
173
|
+
let stopping = false;
|
|
174
|
+
let settled = false;
|
|
175
|
+
let processExitConfirmed = false;
|
|
176
|
+
let exitCode = null;
|
|
177
|
+
let exitSignal = null;
|
|
178
|
+
let terminationSignalSent;
|
|
179
|
+
let terminationError;
|
|
180
|
+
let grace;
|
|
181
|
+
let missingPipes;
|
|
182
|
+
const deadlineError = () => new Error("Windows security command exceeded its inspection deadline");
|
|
183
|
+
const timeout = setTimeout(() => {
|
|
184
|
+
timedOut = true;
|
|
185
|
+
fail(deadlineError());
|
|
186
|
+
}, DEFAULT_PERMISSION_EXEC_TIMEOUT_MS);
|
|
187
|
+
const finish = (outputClosed) => {
|
|
188
|
+
if (settled)
|
|
189
|
+
return;
|
|
190
|
+
settled = true;
|
|
191
|
+
clearTimeout(timeout);
|
|
192
|
+
clearTimeout(grace);
|
|
193
|
+
clearImmediate(missingPipes);
|
|
194
|
+
child.removeListener("exit", onExit);
|
|
195
|
+
child.removeListener("close", onClose);
|
|
196
|
+
child.removeListener("error", onChildError);
|
|
197
|
+
child.on("error", ignoreLateError);
|
|
198
|
+
const cleanupErrors = [];
|
|
199
|
+
for (const [stream, collect] of [[child.stdout, collectOutput], [child.stderr, collectErrors]]) {
|
|
200
|
+
if (!stream)
|
|
201
|
+
continue;
|
|
202
|
+
stream.removeListener("data", collect);
|
|
203
|
+
stream.removeListener("error", fail);
|
|
204
|
+
stream.on("error", ignoreLateError);
|
|
205
|
+
if (!outputClosed) {
|
|
206
|
+
try {
|
|
207
|
+
stream.destroy();
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
cleanupErrors.push(error);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!outputClosed && !processExitConfirmed) {
|
|
215
|
+
try {
|
|
216
|
+
child.unref();
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
cleanupErrors.push(error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (failed || exitCode !== 0 || !outputClosed) {
|
|
223
|
+
reject(new WindowsSecurityCommandError(file, performance.now() - startedAt, {
|
|
224
|
+
cause: failure, pid: child.pid ?? null, code: exitCode, signal: exitSignal, stderr: Buffer.concat(errors),
|
|
225
|
+
processExitConfirmed, outputClosed, terminationSignalSent, terminationError, cleanupErrors,
|
|
226
|
+
...(operation === "create" ? { creationOutcome: "unconfirmed" } : {}),
|
|
227
|
+
}, timedOut));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
try {
|
|
231
|
+
resolve(parseReply(Buffer.concat(output).toString("utf8"), operation));
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
reject(error);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
output.length = 0;
|
|
238
|
+
errors.length = 0;
|
|
239
|
+
};
|
|
240
|
+
const fail = (error) => {
|
|
241
|
+
if (settled)
|
|
242
|
+
return;
|
|
243
|
+
if (!failed) {
|
|
244
|
+
failed = true;
|
|
245
|
+
failure = error;
|
|
246
|
+
}
|
|
247
|
+
if (stopping)
|
|
248
|
+
return;
|
|
249
|
+
stopping = true;
|
|
250
|
+
clearTimeout(timeout);
|
|
251
|
+
// kill() can synchronously emit error, throw, or return false. None proves exit.
|
|
252
|
+
grace = setTimeout(() => finish(false), TERMINATION_GRACE_MS);
|
|
253
|
+
if (!processExitConfirmed) {
|
|
254
|
+
try {
|
|
255
|
+
terminationSignalSent = child.kill("SIGKILL");
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
terminationError ??= error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const collect = (chunks) => (chunk) => {
|
|
263
|
+
if (failed || settled)
|
|
264
|
+
return;
|
|
265
|
+
bytes += chunk.length;
|
|
266
|
+
if (bytes <= MAX_OUTPUT_BYTES)
|
|
267
|
+
chunks.push(chunk);
|
|
268
|
+
else
|
|
269
|
+
fail(new Error("Windows security command exceeded its output budget"));
|
|
270
|
+
};
|
|
271
|
+
const collectOutput = collect(output);
|
|
272
|
+
const collectErrors = collect(errors);
|
|
273
|
+
const onChildError = (error) => {
|
|
274
|
+
if (settled)
|
|
275
|
+
return;
|
|
276
|
+
if (stopping)
|
|
277
|
+
terminationError ??= error;
|
|
278
|
+
else
|
|
279
|
+
fail(error);
|
|
280
|
+
};
|
|
281
|
+
const onExit = (code, signal) => {
|
|
282
|
+
processExitConfirmed = true;
|
|
283
|
+
exitCode = code;
|
|
284
|
+
exitSignal = signal;
|
|
285
|
+
};
|
|
286
|
+
const onClose = (code, signal) => {
|
|
287
|
+
onExit(code, signal);
|
|
288
|
+
if (!failed && performance.now() - startedAt >= DEFAULT_PERMISSION_EXEC_TIMEOUT_MS) {
|
|
289
|
+
timedOut = true;
|
|
290
|
+
failed = true;
|
|
291
|
+
failure = deadlineError();
|
|
292
|
+
}
|
|
293
|
+
finish(true);
|
|
294
|
+
};
|
|
295
|
+
child.on("error", onChildError);
|
|
296
|
+
child.once("exit", onExit);
|
|
297
|
+
// Success still joins exit and both pipes. Failure releases only owned endpoints
|
|
298
|
+
// after grace; the child inherited its own HANDLE, independent of the caller fd.
|
|
299
|
+
child.once("close", onClose);
|
|
300
|
+
child.stdout?.on("error", fail);
|
|
301
|
+
child.stderr?.on("error", fail);
|
|
302
|
+
child.stdout?.on("data", collectOutput);
|
|
303
|
+
child.stderr?.on("data", collectErrors);
|
|
304
|
+
if (!child.stdout || !child.stderr) {
|
|
305
|
+
// Node reports spawn errors on nextTick, which can follow the current microtask.
|
|
306
|
+
missingPipes = setImmediate(() => { if (!failed && !settled)
|
|
307
|
+
fail(new Error("Windows security command output pipes are unavailable")); });
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
export function readWindowsSecurityFactsCommand(targetPath) {
|
|
312
|
+
return parseWindowsSecurityCommandFacts(executeSync("path", { targetPath }));
|
|
313
|
+
}
|
|
314
|
+
export async function inspectWindowsDescriptorCommand(fd) {
|
|
315
|
+
const response = await execute("descriptor", { fd });
|
|
316
|
+
if (!record(response) || typeof response.identity !== "string") {
|
|
317
|
+
unverified("Windows security command returned incomplete handle facts");
|
|
318
|
+
}
|
|
319
|
+
return { identity: response.identity, security: parseWindowsSecurityCommandFacts(response.security) };
|
|
320
|
+
}
|
|
321
|
+
export async function createPrivateWindowsDirectoryCommand(targetPath, expectedParentIdentity) {
|
|
322
|
+
return fullIdentityReceipt(await execute("create", { targetPath, expectedParentIdentity }));
|
|
323
|
+
}
|
|
324
|
+
export function createPrivateWindowsDirectoryCommandSync(targetPath, expectedParentIdentity) {
|
|
325
|
+
return fullIdentityReceipt(executeSync("create", { targetPath, expectedParentIdentity }));
|
|
326
|
+
}
|
|
327
|
+
function fullIdentityReceipt(response, expected) {
|
|
328
|
+
if (!record(response) || !isFullIdentity(response.identity)) {
|
|
329
|
+
unverified("Windows security command returned an incomplete file identity");
|
|
330
|
+
}
|
|
331
|
+
if (expected !== undefined && response.identity !== expected) {
|
|
332
|
+
throw new FsSafeError("path-mismatch", "Windows security command observed a different file");
|
|
333
|
+
}
|
|
334
|
+
return { identity: response.identity };
|
|
335
|
+
}
|
|
336
|
+
export function inspectWindowsDirectoryCommandSync(targetPath, requirePrivate) {
|
|
337
|
+
return fullIdentityReceipt(executeSync("directory", { targetPath, requirePrivate }));
|
|
338
|
+
}
|
|
339
|
+
export async function inspectWindowsDirectoryCommand(targetPath, requirePrivate) {
|
|
340
|
+
return fullIdentityReceipt(await execute("directory", { targetPath, requirePrivate }));
|
|
341
|
+
}
|
|
342
|
+
export function protectPrivateWindowsFileCommandSync(fd, targetPath, expectedParentIdentity) {
|
|
343
|
+
assertIdentity(expectedParentIdentity);
|
|
344
|
+
return fullIdentityReceipt(executeSync("protect-file", { fd, targetPath, expectedParentIdentity }));
|
|
345
|
+
}
|
|
346
|
+
export async function protectPrivateWindowsFileCommand(fd, targetPath, expectedParentIdentity) {
|
|
347
|
+
assertIdentity(expectedParentIdentity);
|
|
348
|
+
return fullIdentityReceipt(await execute("protect-file", { fd, targetPath, expectedParentIdentity }));
|
|
349
|
+
}
|
|
350
|
+
export function verifyPrivateWindowsFileCommandSync(fd, targetPath, expectedFileIdentity, expectedParentIdentity, expectedLinks = 1) {
|
|
351
|
+
assertIdentity(expectedFileIdentity);
|
|
352
|
+
assertIdentity(expectedParentIdentity);
|
|
353
|
+
fullIdentityReceipt(executeSync("verify-file", {
|
|
354
|
+
fd, targetPath, expectedFileIdentity, expectedParentIdentity, expectedLinks,
|
|
355
|
+
}), expectedFileIdentity);
|
|
356
|
+
}
|
|
357
|
+
export async function verifyPrivateWindowsFileCommand(fd, targetPath, expectedFileIdentity, expectedParentIdentity, expectedLinks = 1) {
|
|
358
|
+
assertIdentity(expectedFileIdentity);
|
|
359
|
+
assertIdentity(expectedParentIdentity);
|
|
360
|
+
fullIdentityReceipt(await execute("verify-file", {
|
|
361
|
+
fd, targetPath, expectedFileIdentity, expectedParentIdentity, expectedLinks,
|
|
362
|
+
}), expectedFileIdentity);
|
|
363
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { NativeWindowsSecurityFacts } from "./native-binding.js";
|
|
2
|
+
/** Raw reporting retains unknown flag bits; secure admission validates them below. */
|
|
3
|
+
export declare function parseWindowsSecurityCommandFacts(value: unknown): NativeWindowsSecurityFacts;
|
|
4
|
+
/** Native and command observations share the same fail-closed admission policy. */
|
|
5
|
+
export declare function validateSecureWindowsSecurityFacts(value: unknown): NativeWindowsSecurityFacts;
|
|
6
|
+
//# sourceMappingURL=windows-security-facts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windows-security-facts.d.ts","sourceRoot":"","sources":["../src/windows-security-facts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAmC,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAmFvG,sFAAsF;AACtF,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,OAAO,GAAG,0BAA0B,CAU3F;AAED,mFAAmF;AACnF,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,OAAO,GAAG,0BAA0B,CAoB7F"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
const SID_RE = /^s-\d+-\d+(?:-\d+)+$/;
|
|
3
|
+
const SYSTEM_SID = "s-1-5-18";
|
|
4
|
+
const ADMINISTRATORS_SID = "s-1-5-32-544";
|
|
5
|
+
const WORLD_SIDS = new Set([
|
|
6
|
+
"s-1-1-0", "s-1-5-11", "s-1-5-32-545", "s-1-5-7",
|
|
7
|
+
"s-1-5-32-546", "s-1-5-4", "s-1-5-2",
|
|
8
|
+
]);
|
|
9
|
+
function unverified(message) {
|
|
10
|
+
throw new FsSafeError("permission-unverified", message);
|
|
11
|
+
}
|
|
12
|
+
function record(value) {
|
|
13
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function uint32(value) {
|
|
16
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 0xffff_ffff;
|
|
17
|
+
}
|
|
18
|
+
function validateAce(value, allowUnknownFlags) {
|
|
19
|
+
if (!record(value) || typeof value.sid !== "string" || !SID_RE.test(value.sid) ||
|
|
20
|
+
!uint32(value.mask) || (value.aceType !== "allow" && value.aceType !== "deny") ||
|
|
21
|
+
!record(value.flags))
|
|
22
|
+
return false;
|
|
23
|
+
const { raw } = value.flags;
|
|
24
|
+
if (!uint32(raw) || (raw & ~(allowUnknownFlags ? 0xff : 0xdf)) !== 0)
|
|
25
|
+
return false;
|
|
26
|
+
const fields = [
|
|
27
|
+
["objectInherit", 0x01], ["containerInherit", 0x02],
|
|
28
|
+
["noPropagateInherit", 0x04], ["inheritOnly", 0x08], ["inherited", 0x10],
|
|
29
|
+
["successfulAccess", 0x40], ["failedAccess", 0x80],
|
|
30
|
+
];
|
|
31
|
+
const flags = value.flags;
|
|
32
|
+
return fields.every(([field, bit]) => typeof flags[field] === "boolean" && flags[field] === ((raw & bit) !== 0));
|
|
33
|
+
}
|
|
34
|
+
function validateDescriptor(value, allowUnknownFlags) {
|
|
35
|
+
if (!record(value) || typeof value.ownerSid !== "string" || !SID_RE.test(value.ownerSid) ||
|
|
36
|
+
typeof value.currentUserSid !== "string" || !SID_RE.test(value.currentUserSid) ||
|
|
37
|
+
typeof value.daclPresent !== "boolean" || typeof value.isLocal !== "boolean" ||
|
|
38
|
+
typeof value.aceListComplete !== "boolean" || !Array.isArray(value.unsupportedAceTypes) ||
|
|
39
|
+
!value.unsupportedAceTypes.every(uint32) || !Array.isArray(value.aces) ||
|
|
40
|
+
!value.aces.every(ace => validateAce(ace, allowUnknownFlags))) {
|
|
41
|
+
unverified("Windows descriptor ACL facts were malformed");
|
|
42
|
+
}
|
|
43
|
+
if ((!value.daclPresent && value.aces.length !== 0) ||
|
|
44
|
+
(value.aceListComplete && value.unsupportedAceTypes.length !== 0)) {
|
|
45
|
+
unverified("Windows descriptor DACL facts were inconsistent");
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
function summarize(facts) {
|
|
50
|
+
const ownerClass = facts.ownerSid === facts.currentUserSid ? "current-user"
|
|
51
|
+
: facts.ownerSid === SYSTEM_SID ? "system"
|
|
52
|
+
: facts.ownerSid === ADMINISTRATORS_SID ? "administrators" : "foreign";
|
|
53
|
+
let worldReadable = !facts.daclPresent;
|
|
54
|
+
let worldWritable = !facts.daclPresent;
|
|
55
|
+
let groupReadable = false;
|
|
56
|
+
let groupWritable = false;
|
|
57
|
+
const trusted = new Set([facts.currentUserSid, SYSTEM_SID, ADMINISTRATORS_SID]);
|
|
58
|
+
for (const ace of facts.aces) {
|
|
59
|
+
if (ace.aceType !== "allow" || ace.flags.inheritOnly || trusted.has(ace.sid))
|
|
60
|
+
continue;
|
|
61
|
+
// Generic and file-specific read/write, delete, and security-control rights.
|
|
62
|
+
const canRead = (ace.mask & 0x9000_0089) !== 0;
|
|
63
|
+
const canWrite = (ace.mask & 0x500d_0156) !== 0;
|
|
64
|
+
if (WORLD_SIDS.has(ace.sid)) {
|
|
65
|
+
worldReadable ||= canRead;
|
|
66
|
+
worldWritable ||= canWrite;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
groupReadable ||= canRead;
|
|
70
|
+
groupWritable ||= canWrite;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { ownerClass, worldReadable, worldWritable, groupReadable, groupWritable };
|
|
74
|
+
}
|
|
75
|
+
/** Raw reporting retains unknown flag bits; secure admission validates them below. */
|
|
76
|
+
export function parseWindowsSecurityCommandFacts(value) {
|
|
77
|
+
const facts = validateDescriptor(value, true);
|
|
78
|
+
if (!record(value) || typeof value.daclProtected !== "boolean") {
|
|
79
|
+
unverified("Windows security command returned malformed descriptor facts");
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
...facts,
|
|
83
|
+
...summarize(facts),
|
|
84
|
+
fallbackRequired: !facts.isLocal || !facts.aceListComplete,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Native and command observations share the same fail-closed admission policy. */
|
|
88
|
+
export function validateSecureWindowsSecurityFacts(value) {
|
|
89
|
+
const descriptor = validateDescriptor(value, false);
|
|
90
|
+
const facts = value;
|
|
91
|
+
const summary = summarize(descriptor);
|
|
92
|
+
const booleans = ["worldWritable", "groupWritable", "worldReadable", "groupReadable", "fallbackRequired"];
|
|
93
|
+
if (!booleans.every(field => typeof facts[field] === "boolean") ||
|
|
94
|
+
typeof facts.ownerClass !== "string") {
|
|
95
|
+
unverified("Windows descriptor ACL facts were malformed");
|
|
96
|
+
}
|
|
97
|
+
if (facts.ownerClass !== summary.ownerClass) {
|
|
98
|
+
unverified("Windows descriptor owner facts were inconsistent");
|
|
99
|
+
}
|
|
100
|
+
if (facts.fallbackRequired || !facts.isLocal || !facts.aceListComplete || facts.unsupportedAceTypes.length !== 0) {
|
|
101
|
+
unverified("Windows descriptor ACL facts were incomplete or unsupported");
|
|
102
|
+
}
|
|
103
|
+
if (facts.worldReadable !== summary.worldReadable || facts.worldWritable !== summary.worldWritable ||
|
|
104
|
+
facts.groupReadable !== summary.groupReadable || facts.groupWritable !== summary.groupWritable) {
|
|
105
|
+
unverified("Windows descriptor ACL summary was inconsistent");
|
|
106
|
+
}
|
|
107
|
+
return facts;
|
|
108
|
+
}
|
package/docs/advanced.md
CHANGED
|
@@ -28,7 +28,8 @@ The exports group into a handful of themes. Each documented helper has its own p
|
|
|
28
28
|
| `pathScope`, `PathScope`, `PathScopeOptions`, `PathScopeResolveOptions` | [path-scope.md](path-scope.md) | Absolute-path boundary helper with `Result`-shaped returns. |
|
|
29
29
|
| `ensureDirectoryWithinRoot` | [path-scope.md](path-scope.md#ensuredir-rel-options) | Create a directory while enforcing the root boundary; same result contract as `pathScope().ensureDir()`. |
|
|
30
30
|
| `resolvePathWithinRoot`, `resolvePathsWithinRoot` | – | Resolve one or many relative paths against a trusted root. |
|
|
31
|
-
| `resolveExistingPathsWithinRoot
|
|
31
|
+
| `resolveExistingPathsWithinRoot` | – | Validate existing regular files inside the root, while allowing missing paths. |
|
|
32
|
+
| `resolveStrictExistingPathsWithinRoot` | – | Require every target to exist as a regular non-symlink file inside the root. |
|
|
32
33
|
| `resolveWritablePathWithinRoot` | – | Resolve a write target inside a root. |
|
|
33
34
|
| `resolveRootPath`, `resolveRootPathSync`, `ResolvedRootPath`, `ROOT_PATH_ALIAS_POLICIES`, `RootPathAliasPolicy` | – | Resolve a root directory honoring alias policy. |
|
|
34
35
|
| `resolvePathViaExistingAncestorSync` | – | Walk to an existing ancestor for paths whose tail does not yet exist. |
|
|
@@ -68,8 +69,9 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
|
|
|
68
69
|
| Export | Page | Notes |
|
|
69
70
|
|---|---|---|
|
|
70
71
|
| `readFileDescriptorBounded`, `readFileDescriptorBoundedSync`, `readFileHandleBounded` | – | Incremental whole-file reads for already-open descriptors/handles. They consume at most `maxBytes + 1`, do not close the input, and throw `FsSafeError("too-large")` on overflow. |
|
|
72
|
+
| `createDirectory`, `createDirectorySync`, `createFileSync` | [Exclusive leaf creation](creation.md) | Create one exclusive entry under an existing trusted parent, optionally with private permissions; file creation returns an owned disposable descriptor. |
|
|
71
73
|
| `readFileWindowFully`, `readFileWindowFullySync`, `ReadFileWindowOptions` | [positional-read.md](positional-read.md) | Fill a caller-owned buffer at an explicit file position, completing short reads and returning the EOF count without moving or closing the descriptor. |
|
|
72
|
-
| `copyFileHandle`, `CopyFileHandleOptions` | [copy.md](copy.md#borrowed-filehandle-transfers) | Copy caller-owned regular
|
|
74
|
+
| `copyFileHandle`, `copyFileDescriptorSync`, `CopyFileHandleOptions` | [copy.md](copy.md#borrowed-filehandle-transfers) | Copy caller-owned regular files through async handles or sync descriptors from position zero with byte limits and synchronous callbacks; preserves cursors and leaves publication and cleanup to the caller. |
|
|
73
75
|
| `overwriteFileHandle`, `OverwriteFileHandleOptions` | [in-place-write.md](in-place-write.md) | Overwrite a borrowed read/write handle with prefix-only preparation and best-effort rollback; preserves its inode, cursor, and caller-owned lifetime. |
|
|
74
76
|
| `openRootFile`, `openRootFileSync`, `canUseRootFileOpen`, `matchRootFileOpenFailure`, related types | – | Low-level root-bounded open; rejects every symlink component by default, with `symlinks: "follow-parents-within-root"` for contained parent aliases or `"follow-within-root"` for final links too. |
|
|
75
77
|
| `appendRegularFile`, `appendRegularFileSync`, `readRegularFile`, `readRegularFileSync`, `statRegularFile`, `statRegularFileSync`, `resolveRegularFileAppendFlags`, `AppendRegularFileOptions`, `RegularFileStatResult` | [regular-file.md](regular-file.md) | Type-checked regular-file I/O. |
|