@openclaw/fs-safe 0.18.1 → 0.19.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 +28 -0
- package/dist/advanced.d.ts +2 -1
- package/dist/advanced.js +1 -1
- package/dist/archive-durability.js +1 -1
- package/dist/archive-merge.js +1 -2
- package/dist/archive-staging.d.ts +1 -2
- package/dist/archive-staging.js +3 -3
- package/dist/archive-zip-preflight.d.ts +0 -2
- package/dist/archive-zip-preflight.js +0 -1
- package/dist/archive.d.ts +6 -3
- package/dist/archive.js +5 -3
- package/dist/directory-guard.d.ts +0 -1
- package/dist/directory-guard.js +0 -3
- package/dist/directory-mode-node.d.ts +20 -2
- package/dist/directory-mode-node.js +77 -1
- package/dist/file-lock-sync-acquisition.js +1 -1
- package/dist/file-lock-sync-root-held.d.ts +4 -11
- package/dist/file-lock-sync-root-io.d.ts +1 -4
- package/dist/file-lock-sync-root-options.d.ts +2 -10
- package/dist/file-lock-sync-root.d.ts +2 -4
- package/dist/file-store-boundary.d.ts +3 -7
- package/dist/file-store-boundary.js +7 -10
- package/dist/file-store-prune.js +3 -2
- package/dist/file-store.js +19 -21
- package/dist/guest-native-python.js +23 -31
- package/dist/guest.js +21 -12
- package/dist/json-durable-queue-ownership.d.ts +1 -5
- package/dist/json-durable-queue.d.ts +0 -1
- package/dist/json-durable-queue.js +2 -7
- package/dist/local-file-descriptor.d.ts +2 -5
- package/dist/local-roots.d.ts +2 -7
- package/dist/native-binding.d.ts +12 -13
- package/dist/permission-exec.d.ts +6 -0
- package/dist/permissions-public.d.ts +2 -1
- package/dist/permissions-windows.d.ts +3 -6
- package/dist/permissions.d.ts +3 -10
- package/dist/permissions.js +0 -1
- package/dist/pinned-mutation-shared-route.d.ts +2 -8
- package/dist/pinned-open.d.ts +1 -0
- package/dist/pinned-open.js +1 -1
- package/dist/replace-directory.js +5 -17
- package/dist/replace-file-copy-fallback.d.ts +1 -8
- package/dist/replace-file-descriptor.d.ts +3 -11
- package/dist/replace-file-descriptor.js +1 -1
- package/dist/retained-directory-replacement.d.ts +1 -0
- package/dist/retained-directory-replacement.js +1 -1
- package/dist/root-context.js +3 -2
- package/dist/root-file-final-admission.d.ts +1 -1
- package/dist/root-file-final-admission.js +1 -6
- package/dist/root-file.js +2 -5
- package/dist/root-move-noreplace.d.ts +2 -7
- package/dist/root-paths.d.ts +2 -6
- package/dist/root-remove-identity.d.ts +1 -3
- package/dist/root-walk.js +8 -3
- package/dist/root-write-admission.js +1 -4
- package/dist/root-write-complete-parent.d.ts +2 -0
- package/dist/root-write-complete-parent.js +1 -1
- package/dist/secret-file.d.ts +6 -2
- package/dist/secret-file.js +12 -16
- package/dist/secret-read-policy.js +1 -1
- package/dist/secure-file-windows.js +5 -14
- package/dist/secure-temp-dir.js +2 -7
- package/dist/sidecar-lock-acquire.js +1 -1
- package/dist/sidecar-lock-admission-parser.d.ts +1 -2
- package/dist/sidecar-lock-handle.d.ts +2 -8
- package/dist/sidecar-lock-policy.d.ts +2 -7
- package/dist/sidecar-lock-reclaim.js +5 -6
- package/dist/sidecar-lock-stale-admission.d.ts +1 -5
- package/dist/stat-observation.js +3 -10
- package/dist/strict-file-identity.d.ts +2 -0
- package/dist/strict-file-identity.js +6 -6
- package/dist/temp-target.js +3 -8
- package/dist/temp-workspace-admission.js +6 -12
- package/dist/temp-workspace-child-admission.js +2 -8
- package/dist/temp-workspace-descriptor.js +1 -2
- package/dist/test-hooks.d.ts +1 -1
- package/dist/windows-owner.d.ts +3 -6
- package/dist/windows-security-bridge.cs +0 -2
- package/dist/windows-security-command.js +1 -4
- package/dist/windows-security-facts.d.ts +1 -0
- package/dist/windows-security-facts.js +2 -2
- package/docs/archive.md +2 -0
- package/docs/copy.md +2 -0
- package/docs/file-store.md +5 -0
- package/docs/guest.md +7 -1
- package/docs/native-helper.md +6 -0
- package/docs/root.md +10 -1
- package/docs/secret-file.md +19 -0
- package/docs/sidecar-lock.md +2 -0
- package/docs/walk.md +12 -0
- package/docs/writing.md +11 -0
- package/package.json +8 -8
- package/dist/directory-mode-owner.d.ts +0 -20
- package/dist/directory-mode-owner.js +0 -78
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.19.0 - 2026-09-24
|
|
6
|
+
|
|
7
|
+
### Highlights
|
|
8
|
+
|
|
9
|
+
- **Strict secret-file durability:** `createSecretFileAtomic()` accepts `durable: "file"` to require every file flush to succeed, including on `EPERM`; parent-directory synchronization remains best effort. ([#644](https://github.com/openclaw/fs-safe/pull/644))
|
|
10
|
+
- **Literal `~` names:** keep FileStore keys, absolute Root reads, discovered walk entries, and ZIP/TAR entries literal instead of treating them as home-directory shorthand. Reads, writes, removal, pruning, extraction, and durable publication select the intended entry. ([#633](https://github.com/openclaw/fs-safe/pull/633))
|
|
11
|
+
- **Native Unix descriptor safety:** reject negative descriptors in low-level root, query, hash, copy, clone, staging, and cleanup calls without Rust panics or working-directory operations. Modern macOS beneath opens report `EBADF` for negative roots instead of `EIO`. ([#640](https://github.com/openclaw/fs-safe/pull/640), [#643](https://github.com/openclaw/fs-safe/pull/643), [#646](https://github.com/openclaw/fs-safe/pull/646))
|
|
12
|
+
|
|
13
|
+
### Fixes
|
|
14
|
+
|
|
15
|
+
- **Guest directory permissions:** cross-device moves preserve mode `000`, including nested directories, without changing other modes' umask behavior. Restore top-level permissions through the retained directory descriptor and preserve published entries on failure. Thanks @SebTardif. ([#616](https://github.com/openclaw/fs-safe/pull/616))
|
|
16
|
+
- **ReFS clone cleanup:** remove ordinary partial output when Windows rejects the ignore-readonly deletion flag. Keep readonly attributes intact and include cleanup failures in the original clone error; readonly files or other processes' open handles can still leave output behind. ([#648](https://github.com/openclaw/fs-safe/pull/648))
|
|
17
|
+
|
|
18
|
+
### Compatibility and documentation
|
|
19
|
+
|
|
20
|
+
- Secret creation's default and boolean durability options retain their behavior, and `writeSecretFileAtomic()` remains boolean-only. Strict file synchronization preserves existing publication and cleanup semantics: a failure after publication can leave a complete file present. ([#644](https://github.com/openclaw/fs-safe/pull/644))
|
|
21
|
+
- `Root.walk("~")` and `Root.walk("~/dir")` expand home shorthand when iteration starts and return admitted canonical paths relative to the Root. Use `./~/dir` for a literal tilde directory, and prefix returned entry paths with `./` when passing them to another Root method. Relative Root `~/name` inputs still expand home; FileStore keys do not. ([#633](https://github.com/openclaw/fs-safe/pull/633))
|
|
22
|
+
- Guest moves still require OS permission to read mode-000 source directories; they do not widen source permissions. If permission restoration fails after publication, the source and published copy remain for caller reconciliation. ([#616](https://github.com/openclaw/fs-safe/pull/616))
|
|
23
|
+
- Clarify that `Root.append()` and `openWritable()` creation modes remain subject to the process umask and do not chmod existing files. This documents existing behavior. ([#636](https://github.com/openclaw/fs-safe/pull/636))
|
|
24
|
+
|
|
25
|
+
## 0.18.2 - 2026-09-22
|
|
26
|
+
|
|
27
|
+
### Fixes
|
|
28
|
+
|
|
29
|
+
- **Lock errors:** preserve Root stale-removal rejections and `null` or `undefined` retry failures instead of masking them with timeouts or `TypeError`s. ([#624](https://github.com/openclaw/fs-safe/pull/624))
|
|
30
|
+
- **Secret-file reads:** report caught `null` or `undefined` inspection and read failures as structured errors instead of internal `TypeError`s. ([#623](https://github.com/openclaw/fs-safe/pull/623))
|
|
31
|
+
- **Secret-file creation:** preserve `null` or `undefined` thrown by parameter getters instead of masking them with an internal `TypeError`. ([#625](https://github.com/openclaw/fs-safe/pull/625))
|
|
32
|
+
|
|
5
33
|
## 0.18.1 - 2026-09-22
|
|
6
34
|
|
|
7
35
|
### Fixes
|
package/dist/advanced.d.ts
CHANGED
|
@@ -33,4 +33,5 @@ export { resolveHomeRelativePath } from "./home-dir.js";
|
|
|
33
33
|
export { appendRegularFile, appendRegularFileSync, readRegularFile, readRegularFileSync, resolveRegularFileAppendFlags, statRegularFile, statRegularFileSync, type AppendRegularFileOptions, type RegularFileStatResult, } from "./regular-file.js";
|
|
34
34
|
export { buildRandomTempFilePath, sanitizeTempFileName, type TempFile, tempFile, withTempFile, } from "./temp-target.js";
|
|
35
35
|
export { writeSiblingTempFile, writeViaSiblingTempPath, type WriteSiblingTempFileOptions, type WriteSiblingTempFileResult, } from "./sibling-temp.js";
|
|
36
|
-
export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, type IcaclsResetCommandOptions, type
|
|
36
|
+
export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, type IcaclsResetCommandOptions, type PermissionExec, type WindowsAclEntry, type WindowsAclSummary, } from "./permissions-windows.js";
|
|
37
|
+
export type { PermissionCommandFailure } from "./permission-exec.js";
|
package/dist/advanced.js
CHANGED
|
@@ -35,4 +35,4 @@ export { resolveHomeRelativePath } from "./home-dir.js";
|
|
|
35
35
|
export { appendRegularFile, appendRegularFileSync, readRegularFile, readRegularFileSync, resolveRegularFileAppendFlags, statRegularFile, statRegularFileSync, } from "./regular-file.js";
|
|
36
36
|
export { buildRandomTempFilePath, sanitizeTempFileName, tempFile, withTempFile, } from "./temp-target.js";
|
|
37
37
|
export { writeSiblingTempFile, writeViaSiblingTempPath, } from "./sibling-temp.js";
|
|
38
|
-
export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, } from "./permissions.js";
|
|
38
|
+
export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, } from "./permissions-windows.js";
|
|
@@ -78,7 +78,7 @@ export async function finalizeArchivePublication(params) {
|
|
|
78
78
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
79
79
|
try {
|
|
80
80
|
await assertGuards(file.guards);
|
|
81
|
-
const opened = __addDisposableResource(env_1, await params.targetRoot.open(file.relativePath
|
|
81
|
+
const opened = __addDisposableResource(env_1, await params.targetRoot.open(`./${file.relativePath}`, { hardlinks: "reject", symlinks: "reject" })
|
|
82
82
|
.catch((error) => {
|
|
83
83
|
if (error instanceof FsSafeError && (error.code === "hardlink" || error.code === "path-alias")) {
|
|
84
84
|
throw createArchiveSymlinkTraversalError(file.relativePath);
|
package/dist/archive-merge.js
CHANGED
|
@@ -3,7 +3,6 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { ownExtractionDestinationMutation } from "./archive-deadline.js";
|
|
5
5
|
import { assertDirectoryIdentityGuard, assertResolvedInsideDestination, createDirectoryIdentityGuard, createArchiveSymlinkTraversalError, preparePrivateArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
|
|
6
|
-
import {} from "./directory-mode-owner.js";
|
|
7
6
|
import { pinNodeDirectoryForMode } from "./directory-mode-node.js";
|
|
8
7
|
import { assertSyncDirectoryGuard, inspectDirectoryIdentitySync } from "./directory-guard.js";
|
|
9
8
|
import { FsSafeError } from "./errors.js";
|
|
@@ -251,7 +250,7 @@ async function mergeTree(params, publication, durable = true, entryUmask = 0) {
|
|
|
251
250
|
}
|
|
252
251
|
};
|
|
253
252
|
}
|
|
254
|
-
await targetRoot.copyIn(relPath
|
|
253
|
+
await targetRoot.copyIn(`./${relPath}`, sourcePath, options);
|
|
255
254
|
check();
|
|
256
255
|
await assertGuards();
|
|
257
256
|
await assertResolvedInsideDestination({
|
|
@@ -3,7 +3,6 @@ import { type AsyncDirectoryGuard } from "./directory-guard.js";
|
|
|
3
3
|
import { type ExtractionDeadline } from "./archive-deadline.js";
|
|
4
4
|
import { ArchiveSecurityError } from "./archive-errors.js";
|
|
5
5
|
export type ArchiveDirectoryGuard = AsyncDirectoryGuard<BigIntStats>;
|
|
6
|
-
export { ArchiveSecurityError, type ArchiveSecurityErrorCode } from "./archive-errors.js";
|
|
7
6
|
export declare function createDirectoryIdentityGuard(dir: string): Promise<ArchiveDirectoryGuard>;
|
|
8
7
|
export declare function assertDirectoryIdentityGuard(guard: ArchiveDirectoryGuard): Promise<void>;
|
|
9
8
|
export declare function prepareArchiveDestinationGuard(destDir: string): Promise<ArchiveDirectoryGuard>;
|
|
@@ -29,5 +28,5 @@ export declare function withStagedArchiveDestination<T>(params: {
|
|
|
29
28
|
stagingDirPrefix?: string;
|
|
30
29
|
run: (stagingDir: string) => Promise<T>;
|
|
31
30
|
}): Promise<T>;
|
|
32
|
-
export { mergeExtractedTreeIntoDestination } from "./archive-merge.js";
|
|
33
31
|
export declare function createArchiveSymlinkTraversalError(originalPath: string): ArchiveSecurityError;
|
|
32
|
+
export {};
|
package/dist/archive-staging.js
CHANGED
|
@@ -21,7 +21,6 @@ const ARCHIVE_STAGING_MODE = 0o700;
|
|
|
21
21
|
function checkExtractionDeadline(deadline) {
|
|
22
22
|
deadline?.check();
|
|
23
23
|
}
|
|
24
|
-
export { ArchiveSecurityError } from "./archive-errors.js";
|
|
25
24
|
function symlinkTraversalError(originalPath) {
|
|
26
25
|
return new ArchiveSecurityError("destination-symlink-traversal", `${ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK}: ${formatErrorDetail(originalPath)}`);
|
|
27
26
|
}
|
|
@@ -134,7 +133,9 @@ export async function assertResolvedInsideDestination(params) {
|
|
|
134
133
|
}
|
|
135
134
|
async function mkdirArchiveOutput(params) {
|
|
136
135
|
try {
|
|
137
|
-
|
|
136
|
+
const relativePath = params.relativePath;
|
|
137
|
+
// Archive names are literal; retain Root.mkdir's admission without home expansion.
|
|
138
|
+
await params.targetRoot.mkdir(relativePath === "~" || relativePath.startsWith("~/") ? `./${relativePath}` : relativePath);
|
|
138
139
|
}
|
|
139
140
|
catch (error) {
|
|
140
141
|
if (error instanceof FsSafeError) {
|
|
@@ -302,7 +303,6 @@ export async function withStagedArchiveDestination(params) {
|
|
|
302
303
|
}
|
|
303
304
|
}
|
|
304
305
|
}
|
|
305
|
-
export { mergeExtractedTreeIntoDestination } from "./archive-merge.js";
|
|
306
306
|
export function createArchiveSymlinkTraversalError(originalPath) {
|
|
307
307
|
return symlinkTraversalError(originalPath);
|
|
308
308
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
import { type ArchiveExtractLimits } from "./archive-limits.js";
|
|
2
2
|
import { type ZipArchiveWithFiles } from "./archive-zip-loader.js";
|
|
3
|
-
export { readZipCentralDirectoryEntryCount } from "./archive-zip-count.js";
|
|
4
|
-
export type { ZipArchiveWithFiles } from "./archive-zip-loader.js";
|
|
5
3
|
export declare function loadZipArchiveWithPreflight(buffer: Buffer | Uint8Array, limits?: ArchiveExtractLimits): Promise<ZipArchiveWithFiles>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, resolveExtractLimits, } from "./archive-limits.js";
|
|
2
2
|
import { admitZipBuffer } from "./archive-zip-admission.js";
|
|
3
3
|
import { loadAdmittedZipArchive } from "./archive-zip-loader.js";
|
|
4
|
-
export { readZipCentralDirectoryEntryCount } from "./archive-zip-count.js";
|
|
5
4
|
export async function loadZipArchiveWithPreflight(buffer, limits) {
|
|
6
5
|
const resolvedLimits = resolveExtractLimits(limits);
|
|
7
6
|
if (buffer.byteLength > resolvedLimits.maxArchiveBytes) {
|
package/dist/archive.d.ts
CHANGED
|
@@ -6,10 +6,13 @@ export { resolveArchiveKind, resolvePackedRootDir, type ArchiveKind } from "./ar
|
|
|
6
6
|
export { readArchiveEntry } from "./archive-read.js";
|
|
7
7
|
export { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, DEFAULT_MAX_ARCHIVE_BYTES_ZIP, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_EXTRACTED_BYTES, DEFAULT_MAX_ENTRY_BYTES, DEFAULT_MAX_META_ENTRY_BYTES, DEFAULT_MAX_ENTRY_PATH_COMPONENTS, type ArchiveExtractLimits, type ArchiveLimitErrorCode, } from "./archive-limits.js";
|
|
8
8
|
export { ArchiveFormatError, type ArchiveFormatErrorCode } from "./archive-errors.js";
|
|
9
|
-
export { ArchiveSecurityError, type ArchiveSecurityErrorCode } from "./archive-
|
|
10
|
-
export { createArchiveSymlinkTraversalError,
|
|
9
|
+
export { ArchiveSecurityError, type ArchiveSecurityErrorCode } from "./archive-errors.js";
|
|
10
|
+
export { createArchiveSymlinkTraversalError, prepareArchiveDestinationDir, prepareArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
|
|
11
|
+
export { mergeExtractedTreeIntoDestination } from "./archive-merge.js";
|
|
11
12
|
export { createTarEntryPreflightChecker, type TarEntryInfo } from "./archive-plan.js";
|
|
12
|
-
export { loadZipArchiveWithPreflight
|
|
13
|
+
export { loadZipArchiveWithPreflight } from "./archive-zip-preflight.js";
|
|
14
|
+
export { readZipCentralDirectoryEntryCount } from "./archive-zip-count.js";
|
|
15
|
+
export type { ZipArchiveWithFiles } from "./archive-zip-loader.js";
|
|
13
16
|
export declare function extractArchive(params: ExtractArchiveOptions): Promise<void>;
|
|
14
17
|
export type InspectTarArchiveOptions = Pick<ExtractArchiveOptions, "archivePath" | "timeoutMs" | "limits" | "entryFilter" | "onFiltered">;
|
|
15
18
|
export type InspectedTarEntry = Readonly<Pick<ArchivePlanEntry, "path" | "kind" | "size">>;
|
package/dist/archive.js
CHANGED
|
@@ -29,10 +29,12 @@ export { resolveArchiveKind, resolvePackedRootDir } from "./archive-kind.js";
|
|
|
29
29
|
export { readArchiveEntry } from "./archive-read.js";
|
|
30
30
|
export { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, DEFAULT_MAX_ARCHIVE_BYTES_ZIP, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_EXTRACTED_BYTES, DEFAULT_MAX_ENTRY_BYTES, DEFAULT_MAX_META_ENTRY_BYTES, DEFAULT_MAX_ENTRY_PATH_COMPONENTS, } from "./archive-limits.js";
|
|
31
31
|
export { ArchiveFormatError } from "./archive-errors.js";
|
|
32
|
-
export { ArchiveSecurityError } from "./archive-
|
|
33
|
-
export { createArchiveSymlinkTraversalError,
|
|
32
|
+
export { ArchiveSecurityError } from "./archive-errors.js";
|
|
33
|
+
export { createArchiveSymlinkTraversalError, prepareArchiveDestinationDir, prepareArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
|
|
34
|
+
export { mergeExtractedTreeIntoDestination } from "./archive-merge.js";
|
|
34
35
|
export { createTarEntryPreflightChecker } from "./archive-plan.js";
|
|
35
|
-
export { loadZipArchiveWithPreflight
|
|
36
|
+
export { loadZipArchiveWithPreflight } from "./archive-zip-preflight.js";
|
|
37
|
+
export { readZipCentralDirectoryEntryCount } from "./archive-zip-count.js";
|
|
36
38
|
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
|
|
37
39
|
const OPEN_WRITE_CREATE_FLAGS = fsConstants.O_WRONLY |
|
|
38
40
|
fsConstants.O_CREAT |
|
|
@@ -36,7 +36,6 @@ export declare function createNearestExistingDirectoryGuard(rootReal: string, ta
|
|
|
36
36
|
export declare function createNearestExistingDirectoryGuard(rootReal: string, targetPath: string, options: {
|
|
37
37
|
bigint: boolean;
|
|
38
38
|
}): Promise<AnyAsyncDirectoryGuard>;
|
|
39
|
-
export declare function createNearestExistingSyncDirectoryGuard(rootReal: string, targetPath: string): SyncDirectoryGuard;
|
|
40
39
|
export declare function inspectDirectoryIdentity(dir: string, expected?: Pick<BigIntStats, "dev" | "ino">): Promise<BigIntStats>;
|
|
41
40
|
export declare function observeDirectoryIdentitySync(dir: string, options: {
|
|
42
41
|
bigint: true;
|
package/dist/directory-guard.js
CHANGED
|
@@ -84,9 +84,6 @@ function assertDirectoryGuard(guard, mode) {
|
|
|
84
84
|
export async function createNearestExistingDirectoryGuard(rootReal, targetPath, options = { bigint: false }) {
|
|
85
85
|
return nearestExistingDirectoryGuard(rootReal, targetPath, "native", options);
|
|
86
86
|
}
|
|
87
|
-
export function createNearestExistingSyncDirectoryGuard(rootReal, targetPath) {
|
|
88
|
-
return nearestExistingDirectoryGuard(rootReal, targetPath, "normalized");
|
|
89
|
-
}
|
|
90
87
|
function nearestExistingDirectoryGuard(rootReal, targetPath, mode, options) {
|
|
91
88
|
assertNoWindowsPathAlias(rootReal, "filesystem");
|
|
92
89
|
assertNoWindowsPathAlias(targetPath, "filesystem");
|
|
@@ -1,5 +1,23 @@
|
|
|
1
|
-
import { type BigIntStats } from "node:fs";
|
|
2
|
-
|
|
1
|
+
import { type BigIntStats, type Stats } from "node:fs";
|
|
2
|
+
export type DirectoryModeChecks = {
|
|
3
|
+
check?: () => void;
|
|
4
|
+
beforeChmod?: () => Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
export type DirectoryModeOwner = {
|
|
7
|
+
verify(check?: () => void): Promise<void>;
|
|
8
|
+
apply(mode: number, checks?: DirectoryModeChecks): Promise<void>;
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
export declare function assertOwnedDirectory(expected: Stats | BigIntStats, actual: Stats | BigIntStats): void;
|
|
12
|
+
/** Serializes use and close: even a queued path-based fd operation retains its descriptor. */
|
|
13
|
+
export declare function ownDirectoryMode(params: {
|
|
14
|
+
inspect: () => Promise<number>;
|
|
15
|
+
chmod: (mode: number) => Promise<void>;
|
|
16
|
+
prepareChmod?: () => Promise<void>;
|
|
17
|
+
verifyChmod?: () => Promise<void>;
|
|
18
|
+
close: () => Promise<void>;
|
|
19
|
+
ignoreChmodError?: boolean;
|
|
20
|
+
}): DirectoryModeOwner;
|
|
3
21
|
/** Darwin descriptor inspection avoids requesting directory-content reads. */
|
|
4
22
|
export declare function nodeDarwinDirectoryMetadataFlags(): number;
|
|
5
23
|
export declare function nodeDirectorySearchOnlyFlags(): {
|
|
@@ -3,7 +3,83 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { inspectDirectoryIdentity, inspectDirectoryIdentitySync } from "./directory-guard.js";
|
|
5
5
|
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
|
-
import {
|
|
6
|
+
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
7
|
+
export function assertOwnedDirectory(expected, actual) {
|
|
8
|
+
if (actual.isSymbolicLink() || !actual.isDirectory()) {
|
|
9
|
+
throw new FsSafeError("not-file", "directory mode target must be a real directory");
|
|
10
|
+
}
|
|
11
|
+
if (!sameFileIdentityForCleanup(expected, actual)) {
|
|
12
|
+
throw new FsSafeError("path-mismatch", "directory changed before its mode could be applied");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Serializes use and close: even a queued path-based fd operation retains its descriptor. */
|
|
16
|
+
export function ownDirectoryMode(params) {
|
|
17
|
+
let pending = Promise.resolve();
|
|
18
|
+
let closing;
|
|
19
|
+
const enqueue = (run) => {
|
|
20
|
+
if (closing)
|
|
21
|
+
return Promise.reject(new FsSafeError("path-mismatch", "directory mode owner is closed"));
|
|
22
|
+
const operation = pending.then(run);
|
|
23
|
+
pending = operation.catch(() => undefined);
|
|
24
|
+
return operation;
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
verify: (check) => enqueue(async () => {
|
|
28
|
+
check?.();
|
|
29
|
+
await params.inspect();
|
|
30
|
+
check?.();
|
|
31
|
+
}),
|
|
32
|
+
apply: (mode, checks = {}) => enqueue(async () => {
|
|
33
|
+
// inspect() reports permission bits only; chmod ignores file-type bits,
|
|
34
|
+
// so tolerate raw stat modes (e.g. S_IFDIR | 0o755) by masking up front.
|
|
35
|
+
mode &= 0o7777;
|
|
36
|
+
checks.check?.();
|
|
37
|
+
const currentMode = await params.inspect();
|
|
38
|
+
checks.check?.();
|
|
39
|
+
if (currentMode === mode && !checks.beforeChmod && !checks.check)
|
|
40
|
+
return;
|
|
41
|
+
if (currentMode !== mode) {
|
|
42
|
+
await params.prepareChmod?.();
|
|
43
|
+
checks.check?.();
|
|
44
|
+
}
|
|
45
|
+
await checks.beforeChmod?.();
|
|
46
|
+
checks.check?.();
|
|
47
|
+
// Hooks/ancestor checks can yield; recheck the original named association.
|
|
48
|
+
await params.inspect();
|
|
49
|
+
checks.check?.();
|
|
50
|
+
let dispatchDeadlineFailure;
|
|
51
|
+
if (currentMode !== mode) {
|
|
52
|
+
checks.check?.();
|
|
53
|
+
try {
|
|
54
|
+
await params.chmod(mode);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (!params.ignoreChmodError)
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
// Expiry cannot release the fd while post-dispatch verification is pending.
|
|
61
|
+
try {
|
|
62
|
+
checks.check?.();
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
dispatchDeadlineFailure = { error };
|
|
66
|
+
}
|
|
67
|
+
await params.verifyChmod?.();
|
|
68
|
+
}
|
|
69
|
+
const finalMode = await params.inspect();
|
|
70
|
+
if (dispatchDeadlineFailure)
|
|
71
|
+
throw dispatchDeadlineFailure.error;
|
|
72
|
+
checks.check?.();
|
|
73
|
+
if (!params.ignoreChmodError && finalMode !== mode) {
|
|
74
|
+
throw new FsSafeError("path-mismatch", "directory final mode could not be verified");
|
|
75
|
+
}
|
|
76
|
+
}),
|
|
77
|
+
close() {
|
|
78
|
+
closing ??= pending.then(params.close);
|
|
79
|
+
return closing;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
7
83
|
/** Darwin descriptor inspection avoids requesting directory-content reads. */
|
|
8
84
|
export function nodeDarwinDirectoryMetadataFlags() {
|
|
9
85
|
if (process.platform !== "darwin" || (process.arch !== "x64" && process.arch !== "arm64")) {
|
|
@@ -66,7 +66,7 @@ export class SyncLockAcquisition {
|
|
|
66
66
|
}
|
|
67
67
|
catch (waitError) {
|
|
68
68
|
// Exhausted backoff must preserve the filesystem denial diagnosis.
|
|
69
|
-
if (waitError
|
|
69
|
+
if (waitError?.code === "file_lock_timeout")
|
|
70
70
|
throw error;
|
|
71
71
|
throw waitError;
|
|
72
72
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type SyncHeldLock } from "./file-lock-sync-admission.js";
|
|
2
|
+
import type { SidecarLockOptionFields } from "./sidecar-lock-types.js";
|
|
1
3
|
import type { FileLockSyncHandle } from "./file-lock-sync.js";
|
|
2
|
-
import { type SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
|
|
3
4
|
import { type FileLockSyncRootAuthority, type FileLockSyncRootPath } from "./file-lock-sync-root.js";
|
|
4
5
|
import { type FileLockSyncRootFileReceipt, type FileLockSyncRootSnapshot } from "./file-lock-sync-root-io.js";
|
|
5
6
|
type RootSyncHeldLockReleaseState = "active" | "releasing" | "exit-cleaning" | "released";
|
|
@@ -7,22 +8,14 @@ type RootSyncHeldLockHandleDisposition = {
|
|
|
7
8
|
held: RootSyncHeldLock;
|
|
8
9
|
released: boolean;
|
|
9
10
|
};
|
|
10
|
-
export type RootSyncHeldLock = {
|
|
11
|
+
export type RootSyncHeldLock = SidecarLockOptionFields<SyncHeldLock & {
|
|
11
12
|
deferredExitReleases?: Set<RootSyncHeldLockHandleDisposition>;
|
|
12
|
-
fd: number | undefined;
|
|
13
|
-
lockPath: string;
|
|
14
|
-
normalizedTargetPath: string;
|
|
15
|
-
parsePayload?: (raw: string) => unknown;
|
|
16
|
-
refCount: number;
|
|
17
|
-
reentrantOwner?: string;
|
|
18
13
|
releaseState: RootSyncHeldLockReleaseState;
|
|
19
14
|
revision: number;
|
|
20
15
|
rootAuthority: FileLockSyncRootAuthority;
|
|
21
16
|
rootPath: FileLockSyncRootPath;
|
|
22
17
|
rootReceipt: FileLockSyncRootFileReceipt;
|
|
23
|
-
|
|
24
|
-
timer?: NodeJS.Timeout;
|
|
25
|
-
};
|
|
18
|
+
}>;
|
|
26
19
|
export declare function readRootSidecarSnapshotSync(rootPath: FileLockSyncRootPath, parsePayload?: (raw: string) => unknown, onOpenFailure?: (error: unknown) => void, expectedReceipt?: FileLockSyncRootFileReceipt): FileLockSyncRootSnapshot | null;
|
|
27
20
|
export declare function getRootSyncHeldLocks(): Map<string, RootSyncHeldLock>;
|
|
28
21
|
export declare function ensureRootSyncExitCleanupRegistered(): void;
|
|
@@ -14,10 +14,7 @@ export type FileLockSyncRootFileReceipt = Readonly<{
|
|
|
14
14
|
identity: ExactIdentity;
|
|
15
15
|
parent: DirectoryReceipt;
|
|
16
16
|
}>;
|
|
17
|
-
export type FileLockSyncRootDirectoryReceipt =
|
|
18
|
-
identity: ExactIdentity;
|
|
19
|
-
parent: DirectoryReceipt;
|
|
20
|
-
}>;
|
|
17
|
+
export type FileLockSyncRootDirectoryReceipt = FileLockSyncRootFileReceipt;
|
|
21
18
|
export type FileLockSyncRootDiskSnapshot = {
|
|
22
19
|
ownershipToken?: never;
|
|
23
20
|
payload: unknown;
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import type { FileLockSyncAcquireOptions } from "./file-lock-sync.js";
|
|
2
|
-
import type {
|
|
3
|
-
type CapturedRootSyncAcquireOptions<TPayload extends Record<string, unknown>> = Readonly<{
|
|
4
|
-
compromiseCheckIntervalMs?: number;
|
|
5
|
-
lockPath?: string;
|
|
6
|
-
onCompromised?: (info: SidecarLockCompromisedInfo) => void;
|
|
2
|
+
import type { SidecarLockRetryOptions } from "./sidecar-lock-types.js";
|
|
3
|
+
type CapturedRootSyncAcquireOptions<TPayload extends Record<string, unknown>> = Readonly<Pick<FileLockSyncAcquireOptions<TPayload>, "compromiseCheckIntervalMs" | "lockPath" | "onCompromised" | "parsePayload" | "payload" | "reentrantOwner" | "staleRecovery" | "timeoutMs"> & {
|
|
7
4
|
optionsReceiver: FileLockSyncAcquireOptions<TPayload>;
|
|
8
|
-
parsePayload?: (raw: string) => unknown;
|
|
9
|
-
payload: () => TPayload;
|
|
10
|
-
reentrantOwner?: string;
|
|
11
5
|
retry: Readonly<SidecarLockRetryOptions>;
|
|
12
6
|
shouldReclaim?: FileLockSyncAcquireOptions<TPayload>["shouldReclaim"];
|
|
13
7
|
shouldRemoveStaleLock?: FileLockSyncAcquireOptions<TPayload>["shouldRemoveStaleLock"];
|
|
14
8
|
staleMs: number;
|
|
15
|
-
staleRecovery?: SidecarLockStaleRecovery;
|
|
16
|
-
timeoutMs?: number;
|
|
17
9
|
}>;
|
|
18
10
|
export declare function captureRootSyncAcquireOptions<TPayload extends Record<string, unknown>>(options: FileLockSyncAcquireOptions<TPayload>): CapturedRootSyncAcquireOptions<TPayload>;
|
|
19
11
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DenyMutationPolicy } from "./deny-mutations.js";
|
|
1
2
|
import { type RootContext } from "./root-context.js";
|
|
2
3
|
import type { Root } from "./root-impl.js";
|
|
3
4
|
import type { RootDefaults } from "./root-options.js";
|
|
@@ -7,10 +8,7 @@ export type FileLockSyncRootAuthority = Readonly<{
|
|
|
7
8
|
adapter: object;
|
|
8
9
|
context: RootContext;
|
|
9
10
|
assertBeforeMutation?: () => void;
|
|
10
|
-
denyMutations?: Readonly<
|
|
11
|
-
paths?: readonly string[];
|
|
12
|
-
prefixes?: readonly string[];
|
|
13
|
-
}>;
|
|
11
|
+
denyMutations?: Readonly<DenyMutationPolicy>;
|
|
14
12
|
hardlinks?: RootDefaults["hardlinks"];
|
|
15
13
|
mutationPolicy: RootPathResolutionPolicy;
|
|
16
14
|
readPolicy: RootPathResolutionPolicy;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Readable } from "node:stream";
|
|
2
|
-
import { type SyncStoreDirectoryReceipt } from "./file-store-sync-directory.js";
|
|
2
|
+
import { ensureSyncStoreDirectory, type SyncStoreDirectoryReceipt } from "./file-store-sync-directory.js";
|
|
3
3
|
import { type Root } from "./root.js";
|
|
4
4
|
import { prepareSecretFileWrite } from "./secret-file.js";
|
|
5
5
|
export type SyncParentGuard = SyncStoreDirectoryReceipt;
|
|
@@ -23,12 +23,8 @@ export declare function ensureParentSync(params: {
|
|
|
23
23
|
filePath: string;
|
|
24
24
|
mode: number;
|
|
25
25
|
}): SyncParentGuard;
|
|
26
|
-
export declare function ensureStoreDirectorySync(params:
|
|
27
|
-
|
|
28
|
-
targetDir: string;
|
|
29
|
-
mode: number;
|
|
30
|
-
messagePrefix: "private store" | "store";
|
|
31
|
-
}): SyncParentGuard;
|
|
26
|
+
export declare function ensureStoreDirectorySync(params: Parameters<typeof ensureSyncStoreDirectory>[0]): SyncParentGuard;
|
|
27
|
+
export declare function literalStoreRootPath(relativePath: string): string;
|
|
32
28
|
export declare function assertRelativePath(relativePath: string): string;
|
|
33
29
|
export declare function resolveStorePath(rootDir: string, relativePath: string): string;
|
|
34
30
|
export declare function assertFileStoreMaxBytes(size: number, limit: number | undefined): void;
|
|
@@ -7,8 +7,7 @@ import { FsSafeError } from "./errors.js";
|
|
|
7
7
|
import { assertSyncStoreDirectoryReceipt, ensureSyncStoreDirectory, } from "./file-store-sync-directory.js";
|
|
8
8
|
import { isPathInside, splitSafeRelativePath } from "./path.js";
|
|
9
9
|
import { resolveOpenedFileRealPathForHandle, root } from "./root.js";
|
|
10
|
-
import {
|
|
11
|
-
import { RootHandle } from "./root-impl.js";
|
|
10
|
+
import { rootFromDirectoryGuard } from "./root-impl.js";
|
|
12
11
|
import { prepareSecretFileWrite } from "./secret-file.js";
|
|
13
12
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
14
13
|
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
@@ -16,7 +15,7 @@ import { readRegularFile } from "./regular-file.js";
|
|
|
16
15
|
import { errorCauseOptions } from "./root-errors.js";
|
|
17
16
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
18
17
|
export async function ensureParentInRoot(scopedRoot, relativePath, mode) {
|
|
19
|
-
const parent = path.posix.dirname(relativePath);
|
|
18
|
+
const parent = literalStoreRootPath(path.posix.dirname(relativePath));
|
|
20
19
|
if (parent === ".") {
|
|
21
20
|
return;
|
|
22
21
|
}
|
|
@@ -32,13 +31,7 @@ export async function openWritableStoreRoot(params) {
|
|
|
32
31
|
export async function openPrivateStoreLockRoot(params) {
|
|
33
32
|
const { parentGuard } = await prepareSecretFileWrite(params);
|
|
34
33
|
// Bind to the admitted parent, never resolve a replacement into a fresh capability.
|
|
35
|
-
return
|
|
36
|
-
rootDir: parentGuard.dir,
|
|
37
|
-
rootGuard: { dir: parentGuard.realPath, realPath: parentGuard.realPath, stat: parentGuard.stat },
|
|
38
|
-
rootReal: parentGuard.realPath,
|
|
39
|
-
rootWithSep: ensureTrailingSep(parentGuard.realPath),
|
|
40
|
-
rootIdentity: { dev: parentGuard.stat.dev, ino: parentGuard.stat.ino },
|
|
41
|
-
}, { hardlinks: "reject" });
|
|
34
|
+
return rootFromDirectoryGuard(parentGuard, { hardlinks: "reject" });
|
|
42
35
|
}
|
|
43
36
|
async function chmodDirectoryInRootBestEffort(scopedRoot, relativePath, mode) {
|
|
44
37
|
const dirPath = await scopedRoot.resolve(relativePath);
|
|
@@ -120,6 +113,10 @@ export function ensureStoreDirectorySync(params) {
|
|
|
120
113
|
assertSyncStoreDirectoryReceipt(guard);
|
|
121
114
|
return guard;
|
|
122
115
|
}
|
|
116
|
+
// Store keys and directory-entry names are literal, unlike Root's home syntax.
|
|
117
|
+
export function literalStoreRootPath(relativePath) {
|
|
118
|
+
return relativePath === "~" || relativePath.startsWith("~/") ? `./${relativePath}` : relativePath;
|
|
119
|
+
}
|
|
123
120
|
export function assertRelativePath(relativePath) {
|
|
124
121
|
const raw = relativePath.trim();
|
|
125
122
|
if (!raw || raw !== relativePath) {
|
package/dist/file-store-prune.js
CHANGED
|
@@ -2,6 +2,7 @@ import fsSync, {} from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { literalStoreRootPath } from "./file-store-boundary.js";
|
|
5
6
|
import { isPathInside } from "./path.js";
|
|
6
7
|
import { realpathSync } from "./realpath.js";
|
|
7
8
|
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
@@ -74,13 +75,13 @@ export async function pruneExpiredStoreEntries(params) {
|
|
|
74
75
|
await assertRootGuard();
|
|
75
76
|
// Keep empty-dir pruning on the same root-bounded remove path as files;
|
|
76
77
|
// the Root fallback handles empty directories without recursive delete.
|
|
77
|
-
await scopedRoot.remove(relativePath, REMOVE_EMPTY_DIRECTORY_OPTIONS).catch(() => undefined);
|
|
78
|
+
await scopedRoot.remove(literalStoreRootPath(relativePath), REMOVE_EMPTY_DIRECTORY_OPTIONS).catch(() => undefined);
|
|
78
79
|
}
|
|
79
80
|
continue;
|
|
80
81
|
}
|
|
81
82
|
if (stat.isFile() && now - stat.mtimeMs > params.options.ttlMs) {
|
|
82
83
|
await assertRootGuard();
|
|
83
|
-
await scopedRoot.remove(relativePath, {
|
|
84
|
+
await scopedRoot.remove(literalStoreRootPath(relativePath), {
|
|
84
85
|
assertBeforeMutation: () => {
|
|
85
86
|
// Removal preparation can outlive the expiry observation above.
|
|
86
87
|
const current = fsSync.lstatSync(fullPath);
|