@openclaw/fs-safe 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +20 -1
  3. package/dist/file-store-boundary.d.ts +27 -0
  4. package/dist/file-store-boundary.d.ts.map +1 -0
  5. package/dist/file-store-boundary.js +164 -0
  6. package/dist/file-store.d.ts.map +1 -1
  7. package/dist/file-store.js +62 -72
  8. package/dist/path.d.ts +1 -0
  9. package/dist/path.d.ts.map +1 -1
  10. package/dist/path.js +6 -3
  11. package/dist/private-file-store.d.ts +5 -7
  12. package/dist/private-file-store.d.ts.map +1 -1
  13. package/dist/private-file-store.js +21 -34
  14. package/dist/root-context.d.ts +23 -0
  15. package/dist/root-context.d.ts.map +1 -0
  16. package/dist/root-context.js +62 -0
  17. package/dist/root-errors.d.ts +4 -0
  18. package/dist/root-errors.d.ts.map +1 -0
  19. package/dist/root-errors.js +21 -0
  20. package/dist/root-impl.d.ts +108 -0
  21. package/dist/root-impl.d.ts.map +1 -0
  22. package/dist/{safe-root.js → root-impl.js} +464 -251
  23. package/dist/root.d.ts +1 -107
  24. package/dist/root.d.ts.map +1 -1
  25. package/dist/root.js +1 -1332
  26. package/docs/advanced.md +100 -0
  27. package/docs/archive.md +199 -0
  28. package/docs/atomic.md +163 -0
  29. package/docs/config.md +65 -0
  30. package/docs/contributing.md +80 -0
  31. package/docs/errors.md +154 -0
  32. package/docs/file-store.md +213 -0
  33. package/docs/filename.md +0 -0
  34. package/docs/index.md +76 -0
  35. package/docs/install-path.md +167 -0
  36. package/docs/install.md +150 -0
  37. package/docs/json-store.md +200 -0
  38. package/docs/json.md +158 -0
  39. package/docs/local-roots.md +164 -0
  40. package/docs/path-scope.md +130 -0
  41. package/docs/path.md +191 -0
  42. package/docs/permissions.md +95 -0
  43. package/docs/private-file-store.md +45 -0
  44. package/docs/python-helper.md +103 -0
  45. package/docs/quickstart.md +147 -0
  46. package/docs/reading.md +174 -0
  47. package/docs/regular-file.md +175 -0
  48. package/docs/root.md +205 -0
  49. package/docs/secret-file.md +154 -0
  50. package/docs/secure-file.md +82 -0
  51. package/docs/security-model.md +99 -0
  52. package/docs/sidecar-lock.md +229 -0
  53. package/docs/store.md +44 -0
  54. package/docs/temp.md +276 -0
  55. package/docs/test-hooks.md +74 -0
  56. package/docs/testing.md +168 -0
  57. package/docs/timing.md +121 -0
  58. package/docs/types.md +155 -0
  59. package/docs/walk.md +63 -0
  60. package/docs/writing.md +222 -0
  61. package/package.json +5 -2
  62. package/dist/archive-utils.d.ts +0 -3
  63. package/dist/archive-utils.d.ts.map +0 -1
  64. package/dist/archive-utils.js +0 -26
  65. package/dist/boundary-file-read.d.ts +0 -44
  66. package/dist/boundary-file-read.d.ts.map +0 -1
  67. package/dist/boundary-file-read.js +0 -129
  68. package/dist/boundary-path.d.ts +0 -39
  69. package/dist/boundary-path.d.ts.map +0 -1
  70. package/dist/boundary-path.js +0 -598
  71. package/dist/fs-pinned-path-helper.d.ts +0 -7
  72. package/dist/fs-pinned-path-helper.d.ts.map +0 -1
  73. package/dist/fs-pinned-path-helper.js +0 -182
  74. package/dist/fs-pinned-write-helper.d.ts +0 -21
  75. package/dist/fs-pinned-write-helper.d.ts.map +0 -1
  76. package/dist/fs-pinned-write-helper.js +0 -263
  77. package/dist/hardlink-guards.d.ts +0 -7
  78. package/dist/hardlink-guards.d.ts.map +0 -1
  79. package/dist/hardlink-guards.js +0 -30
  80. package/dist/install-safe-path.d.ts +0 -20
  81. package/dist/install-safe-path.d.ts.map +0 -1
  82. package/dist/install-safe-path.js +0 -94
  83. package/dist/json-file.d.ts +0 -3
  84. package/dist/json-file.d.ts.map +0 -1
  85. package/dist/json-file.js +0 -123
  86. package/dist/json-files.d.ts +0 -20
  87. package/dist/json-files.d.ts.map +0 -1
  88. package/dist/json-files.js +0 -153
  89. package/dist/path-alias-guards.d.ts +0 -19
  90. package/dist/path-alias-guards.d.ts.map +0 -1
  91. package/dist/path-alias-guards.js +0 -21
  92. package/dist/path-guards.d.ts +0 -7
  93. package/dist/path-guards.d.ts.map +0 -1
  94. package/dist/path-guards.js +0 -49
  95. package/dist/path-safety.d.ts +0 -12
  96. package/dist/path-safety.d.ts.map +0 -1
  97. package/dist/path-safety.js +0 -50
  98. package/dist/safe-open-sync.d.ts +0 -24
  99. package/dist/safe-open-sync.d.ts.map +0 -1
  100. package/dist/safe-open-sync.js +0 -71
  101. package/dist/safe-root.d.ts +0 -123
  102. package/dist/safe-root.d.ts.map +0 -1
  103. package/dist/secure-temp-workspace.d.ts +0 -25
  104. package/dist/secure-temp-workspace.d.ts.map +0 -1
  105. package/dist/secure-temp-workspace.js +0 -136
  106. package/dist/sibling-temp-file.d.ts +0 -16
  107. package/dist/sibling-temp-file.d.ts.map +0 -1
  108. package/dist/sibling-temp-file.js +0 -73
  109. package/dist/sibling-temp-write.d.ts +0 -8
  110. package/dist/sibling-temp-write.d.ts.map +0 -1
  111. package/dist/sibling-temp-write.js +0 -40
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2 - 2026-05-06
4
+
5
+ ### Fixes
6
+
7
+ - Reject `fileStore()` and `fileStoreSync()` writes through symlinked parent directories so store commits cannot escape the configured root.
8
+
9
+ ### Tests
10
+
11
+ - Increased filesystem edge coverage around secure temp fallback handling, sibling-temp cleanup, local-root resolution, file locks, and file identity checks.
12
+ - Prevented POSIX test runs from leaving Windows-style secure-temp fallback paths in the repository root.
13
+
14
+ ### Docs
15
+
16
+ - Added missing docs pages for `@openclaw/fs-safe/config`, `@openclaw/fs-safe/store`, `@openclaw/fs-safe/advanced`, and `@openclaw/fs-safe/test-hooks`.
17
+ - Corrected path-helper docs for the synchronous `isPathInsideWithRealpath` and `safeRealpathSync` behavior.
18
+ - Included the Markdown docs in the npm package so README links resolve after install.
19
+
3
20
  ## 0.1.1 - 2026-05-06
4
21
 
5
22
  ### Fixes
package/README.md CHANGED
@@ -1,4 +1,10 @@
1
- # @openclaw/fs-safe
1
+ # 🛡️ @openclaw/fs-safe
2
+
3
+ [![npm](https://img.shields.io/npm/v/@openclaw/fs-safe.svg?color=10b981&label=npm)](https://www.npmjs.com/package/@openclaw/fs-safe)
4
+ [![ci](https://github.com/openclaw/fs-safe/actions/workflows/ci.yml/badge.svg)](https://github.com/openclaw/fs-safe/actions/workflows/ci.yml)
5
+ [![node](https://img.shields.io/node/v/@openclaw/fs-safe.svg?color=10b981)](https://nodejs.org)
6
+ [![license](https://img.shields.io/npm/l/@openclaw/fs-safe.svg?color=10b981)](LICENSE)
7
+ [![docs](https://img.shields.io/badge/docs-fs--safe.io-10b981)](https://fs-safe.io)
2
8
 
3
9
  Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.
4
10
 
@@ -14,6 +20,12 @@ await fs.write("../escape.txt", "x"); // throws FsSafeError("outside-
14
20
 
15
21
  That's the whole pitch. `root()` is the product; the rest of the package — JSON stores, atomic writes, secret files, archive extraction, temp workspaces — is supporting cast for the same boundary.
16
22
 
23
+ Full docs and reference at **[fs-safe.io](https://fs-safe.io)**.
24
+
25
+ ## Contents
26
+
27
+ [Why this exists](#why-this-exists) · [Not a sandbox](#not-a-sandbox) · [Install](#install) · [Quick start](#quick-start) · [Reading](#reading) · [Subpaths](#subpaths) · [Failure semantics](#failure-semantics-in-the-name) · [Atomic writes](#atomic-writes) · [Stores](#stores) · [Secure absolute reads](#secure-absolute-file-reads) · [Walking](#directory-walking) · [Archive extraction](#archive-extraction) · [Path scopes](#advanced-path-scopes) · [Errors](#errors) · [Safety model](#safety-model) · [Limitations](#limitations)
28
+
17
29
  ## Why this exists
18
30
 
19
31
  Most Node code that has to touch caller-controlled paths reaches for:
@@ -26,6 +38,13 @@ That validates a *string*. It does not pin the file you opened, defend against a
26
38
 
27
39
  The same idea has landed in other languages. Go [added `os.Root` and `OpenInRoot`](https://go.dev/blog/osroot); Rust has had [`cap-std`](https://github.com/bytecodealliance/cap-std) for years. Node's `fs` is path-string-oriented and exposes flags like `O_NOFOLLOW` but not an ergonomic "operate inside this root" API. `fs-safe` fills that gap.
28
40
 
41
+ | | Root boundary | Atomic writes | Symlink/hardlink defense | TOCTOU resistance | Archive extraction |
42
+ |---|---|---|---|---|---|
43
+ | `path.resolve().startsWith()` | string check only | – | – | – | – |
44
+ | [`write-file-atomic`](https://www.npmjs.com/package/write-file-atomic) | – | ✓ | – | – | – |
45
+ | Go [`os.Root`](https://go.dev/blog/osroot) / Rust [`cap-std`](https://github.com/bytecodealliance/cap-std) | ✓ | platform | ✓ | ✓ | – |
46
+ | **`@openclaw/fs-safe`** | **✓** | **✓** | **✓** | **✓ (POSIX fd-relative)** | **✓ (ZIP/TAR)** |
47
+
29
48
  ## Not a sandbox
30
49
 
31
50
  This is a **library-level guardrail**, not OS-level isolation. It does not replace containers, seccomp, AppArmor, or filesystem permissions. It is for code that already runs with the privileges of its workspace and wants to stop trivial path tricks from escaping it. If your threat model is a hostile process, you need OS isolation; if your threat model is "an agent, plugin, upload handler, or CLI will eventually be tricked into writing somewhere it shouldn't," `fs-safe` catches that.
@@ -0,0 +1,27 @@
1
+ import { type Readable } from "node:stream";
2
+ import { type Root } from "./root.js";
3
+ export type SyncParentGuard = {
4
+ dir: string;
5
+ realPath: string;
6
+ };
7
+ export declare function ensureParentInRoot(scopedRoot: Root, relativePath: string, mode: number): Promise<void>;
8
+ export declare function openWritableStoreRoot(params: {
9
+ rootDir: string;
10
+ dirMode: number;
11
+ maxBytes?: number;
12
+ }): Promise<Root>;
13
+ export declare function writeStreamToTempSource(params: {
14
+ stream: Readable;
15
+ maxBytes?: number;
16
+ mode: number;
17
+ }): Promise<{
18
+ path: string;
19
+ cleanup: () => Promise<void>;
20
+ }>;
21
+ export declare function assertSyncDirectoryGuard(guard: SyncParentGuard): void;
22
+ export declare function ensureParentSync(params: {
23
+ rootDir: string;
24
+ filePath: string;
25
+ mode: number;
26
+ }): SyncParentGuard;
27
+ //# sourceMappingURL=file-store-boundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-store-boundary.d.ts","sourceRoot":"","sources":["../src/file-store-boundary.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAIvD,OAAO,EAA4C,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGhF,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAOF,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,IAAI,EAChB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AA+CD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAuC1D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CASrE;AAUD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,eAAe,CAwClB"}
@@ -0,0 +1,164 @@
1
+ import syncFs from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { Transform } from "node:stream";
5
+ import { pipeline } from "node:stream/promises";
6
+ import { FsSafeError } from "./errors.js";
7
+ import { isPathInside } from "./path.js";
8
+ import { resolveOpenedFileRealPathForHandle, root } from "./root.js";
9
+ import { resolveSecureTempRoot } from "./secure-temp-dir.js";
10
+ function parentRelativePath(relativePath) {
11
+ const parent = path.posix.dirname(relativePath);
12
+ return parent === "." ? "" : parent;
13
+ }
14
+ export async function ensureParentInRoot(scopedRoot, relativePath, mode) {
15
+ const parent = parentRelativePath(relativePath);
16
+ if (!parent) {
17
+ return;
18
+ }
19
+ await scopedRoot.mkdir(parent);
20
+ await chmodDirectoryInRootBestEffort(scopedRoot, parent, mode).catch(() => undefined);
21
+ }
22
+ export async function openWritableStoreRoot(params) {
23
+ await fs.mkdir(params.rootDir, { recursive: true, mode: params.dirMode });
24
+ await fs.chmod(params.rootDir, params.dirMode).catch(() => undefined);
25
+ return await root(params.rootDir, { hardlinks: "reject", maxBytes: params.maxBytes });
26
+ }
27
+ async function chmodDirectoryInRootBestEffort(scopedRoot, relativePath, mode) {
28
+ const dirPath = await scopedRoot.resolve(relativePath);
29
+ const directoryFlag = "O_DIRECTORY" in syncFs.constants ? syncFs.constants.O_DIRECTORY : 0;
30
+ const noFollowFlag = process.platform !== "win32" && "O_NOFOLLOW" in syncFs.constants
31
+ ? syncFs.constants.O_NOFOLLOW
32
+ : 0;
33
+ const handle = await fs.open(dirPath, syncFs.constants.O_RDONLY | directoryFlag | noFollowFlag);
34
+ try {
35
+ const stat = await handle.stat();
36
+ if (!stat.isDirectory()) {
37
+ return;
38
+ }
39
+ const realPath = await resolveOpenedFileRealPathForHandle(handle, dirPath);
40
+ if (!isPathInside(scopedRoot.rootWithSep, realPath)) {
41
+ throw new FsSafeError("outside-workspace", "directory is outside store root");
42
+ }
43
+ await handle.chmod(mode).catch(() => undefined);
44
+ }
45
+ finally {
46
+ await handle.close().catch(() => undefined);
47
+ }
48
+ }
49
+ function createMaxBytesTransform(maxBytes) {
50
+ if (maxBytes === undefined) {
51
+ return undefined;
52
+ }
53
+ let total = 0;
54
+ return new Transform({
55
+ transform(chunk, _encoding, callback) {
56
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
57
+ total += buffer.byteLength;
58
+ if (total > maxBytes) {
59
+ callback(new FsSafeError("too-large", `file exceeds maximum size of ${maxBytes} bytes`));
60
+ return;
61
+ }
62
+ callback(null, buffer);
63
+ },
64
+ });
65
+ }
66
+ export async function writeStreamToTempSource(params) {
67
+ const tempRoot = resolveSecureTempRoot({
68
+ fallbackPrefix: "fs-safe-file-store",
69
+ unsafeFallbackLabel: "file store temp dir",
70
+ warn: () => undefined,
71
+ });
72
+ const dir = await fs.mkdtemp(path.join(tempRoot, "fs-safe-file-store-"));
73
+ const filePath = path.join(dir, "payload");
74
+ let handle = null;
75
+ let handleClosedByStream = false;
76
+ try {
77
+ handle = await fs.open(filePath, "wx", params.mode);
78
+ const writable = handle.createWriteStream();
79
+ writable.once("close", () => {
80
+ handleClosedByStream = true;
81
+ });
82
+ const limiter = createMaxBytesTransform(params.maxBytes);
83
+ if (limiter) {
84
+ await pipeline(params.stream, limiter, writable);
85
+ }
86
+ else {
87
+ await pipeline(params.stream, writable);
88
+ }
89
+ if (!handleClosedByStream) {
90
+ await handle.close().catch(() => undefined);
91
+ }
92
+ await fs.chmod(filePath, params.mode).catch(() => undefined);
93
+ return {
94
+ path: filePath,
95
+ cleanup: async () => {
96
+ await fs.rm(dir, { recursive: true, force: true }).catch(() => undefined);
97
+ },
98
+ };
99
+ }
100
+ catch (err) {
101
+ if (handle && !handleClosedByStream) {
102
+ await handle.close().catch(() => undefined);
103
+ }
104
+ await fs.rm(dir, { recursive: true, force: true }).catch(() => undefined);
105
+ throw err;
106
+ }
107
+ }
108
+ export function assertSyncDirectoryGuard(guard) {
109
+ const stat = syncFs.lstatSync(guard.dir);
110
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
111
+ throw new FsSafeError("not-file", `store directory component must be a directory: ${guard.dir}`);
112
+ }
113
+ const realPath = syncFs.realpathSync(guard.dir);
114
+ if (realPath !== guard.realPath) {
115
+ throw new FsSafeError("path-mismatch", "store directory changed during write");
116
+ }
117
+ }
118
+ function chmodDirectorySyncBestEffort(dir, mode) {
119
+ try {
120
+ syncFs.chmodSync(dir, mode);
121
+ }
122
+ catch {
123
+ // Best-effort on platforms that do not enforce POSIX modes.
124
+ }
125
+ }
126
+ export function ensureParentSync(params) {
127
+ const rootDir = path.resolve(params.rootDir);
128
+ const dir = path.dirname(path.resolve(params.filePath));
129
+ const relative = path.relative(rootDir, dir);
130
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
131
+ throw new FsSafeError("outside-workspace", "file path escapes store root");
132
+ }
133
+ syncFs.mkdirSync(rootDir, { recursive: true, mode: params.mode });
134
+ const rootStat = syncFs.lstatSync(rootDir);
135
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
136
+ throw new FsSafeError("not-file", `store root must be a directory: ${rootDir}`);
137
+ }
138
+ const rootReal = syncFs.realpathSync(rootDir);
139
+ chmodDirectorySyncBestEffort(rootDir, params.mode);
140
+ let current = rootDir;
141
+ for (const segment of path.relative(rootDir, dir).split(path.sep).filter(Boolean)) {
142
+ current = path.join(current, segment);
143
+ try {
144
+ const stat = syncFs.lstatSync(current);
145
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
146
+ throw new FsSafeError("not-file", `store directory component must be a directory: ${current}`);
147
+ }
148
+ }
149
+ catch (error) {
150
+ if (error.code !== "ENOENT") {
151
+ throw error;
152
+ }
153
+ syncFs.mkdirSync(current, { mode: params.mode });
154
+ }
155
+ const currentReal = syncFs.realpathSync(current);
156
+ if (!isPathInside(rootReal, currentReal)) {
157
+ throw new FsSafeError("outside-workspace", "store directory escapes root");
158
+ }
159
+ chmodDirectorySyncBestEffort(current, params.mode);
160
+ }
161
+ const guard = { dir, realPath: syncFs.realpathSync(dir) };
162
+ assertSyncDirectoryGuard(guard);
163
+ return guard;
164
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"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"}
@@ -2,13 +2,12 @@ import { randomUUID } from "node:crypto";
2
2
  import syncFs from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { pipeline } from "node:stream/promises";
6
5
  import { FsSafeError } from "./errors.js";
6
+ import { assertSyncDirectoryGuard, ensureParentInRoot, ensureParentSync, openWritableStoreRoot, writeStreamToTempSource, } from "./file-store-boundary.js";
7
7
  import { createJsonStore } from "./json-document-store.js";
8
8
  import { isPathInside, resolveSafeRelativePath } from "./path.js";
9
9
  import { root } from "./root.js";
10
10
  import { writeSecretFileAtomic } from "./secret-file.js";
11
- import { writeSiblingTempFile } from "./sibling-temp.js";
12
11
  function assertRelativePath(relativePath) {
13
12
  const raw = relativePath.trim();
14
13
  if (!raw) {
@@ -24,11 +23,6 @@ function assertStoreFilePath(rootDir, filePath) {
24
23
  throw new FsSafeError("outside-workspace", "file path escapes store root");
25
24
  }
26
25
  }
27
- async function ensureParent(filePath, mode) {
28
- const dir = path.dirname(filePath);
29
- await fs.mkdir(dir, { recursive: true, mode });
30
- await fs.chmod(dir, mode).catch(() => undefined);
31
- }
32
26
  function assertMaxBytes(size, maxBytes) {
33
27
  if (maxBytes !== undefined && size > maxBytes) {
34
28
  throw new FsSafeError("too-large", `file exceeds maximum size of ${maxBytes} bytes`);
@@ -41,26 +35,26 @@ function isNotFound(error) {
41
35
  error.code === "ENOTDIR";
42
36
  }
43
37
  async function copyIntoRoot(params) {
44
- const destination = resolveStorePath(params.rootDir, params.relativePath);
38
+ const relativePath = assertRelativePath(params.relativePath);
39
+ const destination = resolveStorePath(params.rootDir, relativePath);
45
40
  const sourceStat = await fs.lstat(params.sourcePath);
46
41
  if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
47
42
  throw new FsSafeError("not-file", "source path is not a file");
48
43
  }
49
44
  assertMaxBytes(sourceStat.size, params.maxBytes);
50
- await ensureParent(destination, params.dirMode ?? 0o700);
51
- const result = await writeSiblingTempFile({
52
- dir: path.dirname(destination),
53
- dirMode: params.dirMode ?? 0o700,
45
+ const dirMode = params.dirMode ?? 0o700;
46
+ const scopedRoot = await openWritableStoreRoot({
47
+ rootDir: params.rootDir,
48
+ dirMode,
49
+ maxBytes: params.maxBytes,
50
+ });
51
+ await ensureParentInRoot(scopedRoot, relativePath, dirMode);
52
+ await scopedRoot.copyIn(relativePath, params.sourcePath, {
53
+ maxBytes: params.maxBytes,
54
+ mkdir: false,
54
55
  mode: params.mode ?? 0o600,
55
- tempPrefix: params.tempPrefix ?? `.${path.basename(destination)}`,
56
- writeTemp: async (tempPath) => {
57
- await fs.copyFile(params.sourcePath, tempPath);
58
- },
59
- resolveFinalPath: () => destination,
60
- syncTempFile: true,
61
- syncParentDir: true,
62
56
  });
63
- return result.filePath;
57
+ return destination;
64
58
  }
65
59
  export function fileStore(options) {
66
60
  const rootDir = path.resolve(options.rootDir);
@@ -72,7 +66,8 @@ export function fileStore(options) {
72
66
  return await root(rootDir, { hardlinks: "reject", maxBytes });
73
67
  }
74
68
  async function write(relativePath, data, writeOptions) {
75
- const destination = resolveStorePath(rootDir, relativePath);
69
+ const safeRelativePath = assertRelativePath(relativePath);
70
+ const destination = resolveStorePath(rootDir, safeRelativePath);
76
71
  const content = Buffer.isBuffer(data) ? data : Buffer.from(data);
77
72
  assertMaxBytes(content.byteLength, writeOptions?.maxBytes ?? maxBytes);
78
73
  if (privateMode) {
@@ -85,20 +80,18 @@ export function fileStore(options) {
85
80
  });
86
81
  return destination;
87
82
  }
88
- await ensureParent(destination, writeOptions?.dirMode ?? dirMode);
89
- const result = await writeSiblingTempFile({
90
- dir: path.dirname(destination),
91
- dirMode: writeOptions?.dirMode ?? dirMode,
83
+ const writeDirMode = writeOptions?.dirMode ?? dirMode;
84
+ const scopedRoot = await openWritableStoreRoot({
85
+ rootDir,
86
+ dirMode: writeDirMode,
87
+ maxBytes: writeOptions?.maxBytes ?? maxBytes,
88
+ });
89
+ await ensureParentInRoot(scopedRoot, safeRelativePath, writeDirMode);
90
+ await scopedRoot.write(safeRelativePath, content, {
91
+ mkdir: false,
92
92
  mode: writeOptions?.mode ?? mode,
93
- tempPrefix: writeOptions?.tempPrefix ?? `.${path.basename(destination)}`,
94
- writeTemp: async (tempPath) => {
95
- await fs.writeFile(tempPath, content);
96
- },
97
- resolveFinalPath: () => destination,
98
- syncTempFile: true,
99
- syncParentDir: true,
100
93
  });
101
- return result.filePath;
94
+ return destination;
102
95
  }
103
96
  return {
104
97
  rootDir,
@@ -106,7 +99,8 @@ export function fileStore(options) {
106
99
  root: openRoot,
107
100
  write,
108
101
  writeStream: async (relativePath, stream, writeOptions) => {
109
- const destination = resolveStorePath(rootDir, relativePath);
102
+ const safeRelativePath = assertRelativePath(relativePath);
103
+ const destination = resolveStorePath(rootDir, safeRelativePath);
110
104
  const limit = writeOptions?.maxBytes ?? maxBytes;
111
105
  if (privateMode) {
112
106
  const chunks = [];
@@ -126,34 +120,26 @@ export function fileStore(options) {
126
120
  });
127
121
  return destination;
128
122
  }
129
- await ensureParent(destination, writeOptions?.dirMode ?? dirMode);
130
- let total = 0;
131
- const result = await writeSiblingTempFile({
132
- dir: path.dirname(destination),
133
- dirMode: writeOptions?.dirMode ?? dirMode,
123
+ const staged = await writeStreamToTempSource({
124
+ stream,
125
+ maxBytes: limit,
134
126
  mode: writeOptions?.mode ?? mode,
135
- tempPrefix: writeOptions?.tempPrefix ?? `.${path.basename(destination)}`,
136
- writeTemp: async (tempPath) => {
137
- const writable = await fs.open(tempPath, "w", writeOptions?.mode ?? mode);
138
- try {
139
- const out = writable.createWriteStream();
140
- stream.on("data", (chunk) => {
141
- total += Buffer.byteLength(chunk);
142
- if (limit !== undefined && total > limit) {
143
- stream.destroy(new FsSafeError("too-large", `file exceeds maximum size of ${limit} bytes`));
144
- }
145
- });
146
- await pipeline(stream, out);
147
- }
148
- finally {
149
- await writable.close().catch(() => undefined);
150
- }
151
- },
152
- resolveFinalPath: () => destination,
153
- syncTempFile: true,
154
- syncParentDir: true,
155
127
  });
156
- return result.filePath;
128
+ try {
129
+ await copyIntoRoot({
130
+ rootDir,
131
+ relativePath: safeRelativePath,
132
+ sourcePath: staged.path,
133
+ maxBytes: limit,
134
+ mode: writeOptions?.mode ?? mode,
135
+ tempPrefix: writeOptions?.tempPrefix,
136
+ dirMode: writeOptions?.dirMode ?? dirMode,
137
+ });
138
+ }
139
+ finally {
140
+ await staged.cleanup();
141
+ }
142
+ return destination;
157
143
  },
158
144
  copyIn: async (relativePath, sourcePath, writeOptions) => privateMode
159
145
  ? await (async () => {
@@ -274,16 +260,6 @@ export function fileStore(options) {
274
260
  },
275
261
  };
276
262
  }
277
- function ensureParentSync(filePath, mode) {
278
- const dir = path.dirname(filePath);
279
- syncFs.mkdirSync(dir, { recursive: true, mode });
280
- try {
281
- syncFs.chmodSync(dir, mode);
282
- }
283
- catch {
284
- // Best-effort on platforms that do not enforce POSIX modes.
285
- }
286
- }
287
263
  function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
288
264
  const root = path.resolve(rootDir);
289
265
  const target = path.resolve(targetDir);
@@ -330,6 +306,7 @@ function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
330
306
  function writeFileSyncAtomic(params) {
331
307
  const filePath = path.resolve(params.filePath);
332
308
  assertStoreFilePath(params.rootDir, filePath);
309
+ let parentGuard;
333
310
  if (params.privateMode) {
334
311
  ensurePrivateDirectorySync(params.rootDir, path.dirname(filePath), params.dirMode);
335
312
  try {
@@ -345,11 +322,18 @@ function writeFileSyncAtomic(params) {
345
322
  }
346
323
  }
347
324
  else {
348
- ensureParentSync(filePath, params.dirMode);
325
+ parentGuard = ensureParentSync({
326
+ rootDir: params.rootDir,
327
+ filePath,
328
+ mode: params.dirMode,
329
+ });
349
330
  }
350
- const tempPath = path.join(path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
331
+ const tempPath = path.join(parentGuard?.dir ?? path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
351
332
  let tempExists = false;
352
333
  try {
334
+ if (parentGuard) {
335
+ assertSyncDirectoryGuard(parentGuard);
336
+ }
353
337
  syncFs.writeFileSync(tempPath, params.content, { flag: "wx", mode: params.mode });
354
338
  tempExists = true;
355
339
  try {
@@ -358,8 +342,14 @@ function writeFileSyncAtomic(params) {
358
342
  catch {
359
343
  // Best-effort on platforms that do not enforce POSIX modes.
360
344
  }
345
+ if (parentGuard) {
346
+ assertSyncDirectoryGuard(parentGuard);
347
+ }
361
348
  syncFs.renameSync(tempPath, filePath);
362
349
  tempExists = false;
350
+ if (parentGuard) {
351
+ assertSyncDirectoryGuard(parentGuard);
352
+ }
363
353
  try {
364
354
  syncFs.chmodSync(filePath, params.mode);
365
355
  }
package/dist/path.d.ts CHANGED
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  export declare function normalizeWindowsPathForComparison(input: string): string;
3
3
  export declare function isNodeError(value: unknown): value is NodeJS.ErrnoException;
4
4
  export declare function hasNodeErrorCode(value: unknown, code: string): boolean;
5
+ export declare function assertNoNulPathInput(filePath: string, message?: string): void;
5
6
  export declare function isNotFoundPathError(value: unknown): boolean;
6
7
  export declare function isSymlinkOpenError(value: unknown): boolean;
7
8
  export declare function isPathInside(root: string, target: string): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAWzB,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASvE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,cAAc,CAI1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CA0BlE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAExE;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAa/F;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAChE,OAAO,CAUT;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG,IAAI,CAMhE;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAyBpE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAOrF"}
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAWzB,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASvE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,cAAc,CAI1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,SAA6B,GAAG,IAAI,CAIjG;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CA0BlE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAExE;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAa/F;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAChE,OAAO,CAUT;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG,IAAI,CAMhE;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAuBpE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAOrF"}
package/dist/path.js CHANGED
@@ -22,6 +22,11 @@ export function isNodeError(value) {
22
22
  export function hasNodeErrorCode(value, code) {
23
23
  return isNodeError(value) && value.code === code;
24
24
  }
25
+ export function assertNoNulPathInput(filePath, message = "path contains a NUL byte") {
26
+ if (filePath.includes("\0")) {
27
+ throw new FsSafeError("invalid-path", message);
28
+ }
29
+ }
25
30
  export function isNotFoundPathError(value) {
26
31
  return isNodeError(value) && typeof value.code === "string" && NOT_FOUND_CODES.has(value.code);
27
32
  }
@@ -94,9 +99,7 @@ export function splitSafeRelativePath(relativePath) {
94
99
  if (relativePath.length === 0 || relativePath === ".") {
95
100
  return [];
96
101
  }
97
- if (relativePath.includes("\0")) {
98
- throw new FsSafeError("invalid-path", "relative path contains a NUL byte");
99
- }
102
+ assertNoNulPathInput(relativePath, "relative path contains a NUL byte");
100
103
  if (relativePath.includes("\\")) {
101
104
  throw new FsSafeError("invalid-path", "relative path must use forward slashes");
102
105
  }
@@ -1,18 +1,16 @@
1
- import { type FileStore } from "./file-store.js";
2
- export type PrivateStateStoreOptions = {
1
+ export type PrivateFileStore = {
3
2
  rootDir: string;
4
- };
5
- export type PrivateStateStore = Omit<FileStore, "readText" | "readJson" | "writeText" | "writeJson"> & {
3
+ path(relativePath: string): string;
6
4
  readText(relativePath: string, options?: {
7
5
  maxBytes?: number;
8
6
  }): Promise<string | null>;
9
7
  readJson<T = unknown>(relativePath: string, options?: {
10
8
  maxBytes?: number;
11
9
  }): Promise<T | null>;
12
- writeText(relativePath: string, content: string | Uint8Array): Promise<string>;
10
+ writeText(relativePath: string, content: string | Uint8Array): Promise<void>;
13
11
  writeJson(relativePath: string, value: unknown, options?: {
14
12
  trailingNewline?: boolean;
15
- }): Promise<string>;
13
+ }): Promise<void>;
16
14
  };
17
15
  export declare function writePrivateTextAtomic(params: {
18
16
  rootDir: string;
@@ -56,5 +54,5 @@ export declare function writePrivateJsonAtomicSync(params: {
56
54
  value: unknown;
57
55
  trailingNewline?: boolean;
58
56
  }): void;
59
- export declare function privateStateStore(options: PrivateStateStoreOptions): PrivateStateStore;
57
+ export declare function privateFileStore(rootDir: string): PrivateFileStore;
60
58
  //# sourceMappingURL=private-file-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"private-file-store.d.ts","sourceRoot":"","sources":["../src/private-file-store.ts"],"names":[],"mappings":"AAIA,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAM5D,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC,GAAG;IACrG,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxF,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAChG,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/E,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3G,CAAC;AAgBF,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;CAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhB;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBzB;AAoBD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,MAAM,GAAG,IAAI,CAiBhB;AAED,wBAAsB,eAAe,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAGpB;AAED,wBAAgB,mBAAmB,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,CAAC,GAAG,IAAI,CAGX;AAoCD,wBAAgB,0BAA0B,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;CAC9B,GAAG,IAAI,CA4CP;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,IAAI,CAOP;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CA2CtF"}
1
+ {"version":3,"file":"private-file-store.d.ts","sourceRoot":"","sources":["../src/private-file-store.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxF,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAChG,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzG,CAAC;AAgBF,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;CAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhB;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBzB;AAoBD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,MAAM,GAAG,IAAI,CAiBhB;AAED,wBAAsB,eAAe,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAGpB;AAED,wBAAgB,mBAAmB,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,CAAC,GAAG,IAAI,CAGX;AAoCD,wBAAgB,0BAA0B,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;CAC9B,GAAG,IAAI,CA4CP;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,IAAI,CAOP;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAiClE"}