@statewalker/webrun-files-composite 0.7.1 → 0.8.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/README.md +219 -144
- package/dist/cjs/index.cjs +454 -28
- package/dist/composite-files-api.d.ts +50 -4
- package/dist/composite-files-api.d.ts.map +1 -1
- package/dist/esm/index.js +449 -29
- package/dist/filtered-files-api.d.ts +167 -0
- package/dist/filtered-files-api.d.ts.map +1 -0
- package/dist/glob-to-regexp.d.ts +56 -0
- package/dist/glob-to-regexp.d.ts.map +1 -0
- package/dist/guarded-files-api.d.ts +64 -0
- package/dist/guarded-files-api.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +50 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/composite-files-api.ts +50 -29
- package/src/filtered-files-api.ts +257 -0
- package/src/glob-to-regexp.ts +182 -0
- package/src/guarded-files-api.ts +131 -0
- package/src/index.ts +10 -0
- package/src/types.ts +52 -3
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { FileInfo, FileStats, FilesApi, ListOptions, ReadOptions } from "@statewalker/webrun-files";
|
|
2
|
+
/**
|
|
3
|
+
* Predicate evaluated against a **normalized** path (single leading slash,
|
|
4
|
+
* no trailing slash) by `FilteredFilesApi` to decide visibility.
|
|
5
|
+
*
|
|
6
|
+
* - `true` → the path is visible (the wrapped operation runs).
|
|
7
|
+
* - `false` → the path is hidden (the wrapper short-circuits as if the path
|
|
8
|
+
* did not exist).
|
|
9
|
+
*
|
|
10
|
+
* The predicate may be synchronous or return a `Promise<boolean>`. When
|
|
11
|
+
* async, it is awaited on each call — so prefer pure / cheap checks.
|
|
12
|
+
*/
|
|
13
|
+
export type PathFilter = (path: string) => boolean | Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Builds a {@link PathFilter} that hides any path whose normalized form
|
|
16
|
+
* equals one of the provided prefixes or lives under `${prefix}/`.
|
|
17
|
+
*
|
|
18
|
+
* Prefixes are normalized through `normalizePath` (so `"foo"`, `"/foo"`, and
|
|
19
|
+
* `"/foo/"` are equivalent). Empty / root entries are dropped — they would
|
|
20
|
+
* otherwise hide every path.
|
|
21
|
+
*
|
|
22
|
+
* Matching is **boundary-aware**: the prefix `"/priv"` does not match the
|
|
23
|
+
* path `"/private"` because there is no `/` boundary between them.
|
|
24
|
+
*
|
|
25
|
+
* @param pathPrefixes Path prefixes whose contents (and the prefix itself)
|
|
26
|
+
* should be hidden. Pass none to hide nothing.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const filter = newPathFilter("/.git", "/node_modules");
|
|
31
|
+
* filter("/src/index.ts"); // true
|
|
32
|
+
* filter("/.git"); // false
|
|
33
|
+
* filter("/.git/HEAD"); // false
|
|
34
|
+
* filter("/notgit"); // true (boundary-aware, no false match)
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function newPathFilter(...pathPrefixes: string[]): PathFilter;
|
|
38
|
+
/**
|
|
39
|
+
* Builds a {@link PathFilter} that hides any path whose normalized form
|
|
40
|
+
* matches at least one of the provided regular expressions.
|
|
41
|
+
*
|
|
42
|
+
* The path is normalized through `normalizePath` before matching, so a
|
|
43
|
+
* regexp anchored on `^/` always sees a leading slash and never a trailing
|
|
44
|
+
* one. The regexp's `lastIndex` is irrelevant — the filter calls `test`
|
|
45
|
+
* via a fresh evaluation each time, but stateful (`/g`, `/y`) regexps
|
|
46
|
+
* still mutate `lastIndex` across calls; pass non-stateful regexps unless
|
|
47
|
+
* you know what you are doing.
|
|
48
|
+
*
|
|
49
|
+
* @param pathRegexps Regular expressions whose match means "hide this
|
|
50
|
+
* path". Pass none to hide nothing.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* // Hide every dotfile and every *.log file
|
|
55
|
+
* const filter = newRegexpPathFilter(/\/\.[^/]+$/, /\.log$/);
|
|
56
|
+
* filter("/src/index.ts"); // true
|
|
57
|
+
* filter("/.env"); // false (matches /\/\.[^/]+$/)
|
|
58
|
+
* filter("/build.log"); // false (matches /\.log$/)
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function newRegexpPathFilter(...pathRegexps: RegExp[]): PathFilter;
|
|
62
|
+
/**
|
|
63
|
+
* Builds a {@link PathFilter} that hides any path whose normalized form
|
|
64
|
+
* matches at least one of the provided glob patterns.
|
|
65
|
+
*
|
|
66
|
+
* Each glob is compiled with `extended: true` and `globstar: true`, the
|
|
67
|
+
* standard "filesystem-style" mode:
|
|
68
|
+
*
|
|
69
|
+
* - `*` matches any number of characters within a single path segment
|
|
70
|
+
* (does **not** cross `/`).
|
|
71
|
+
* - `**` between slashes matches zero or more whole path segments.
|
|
72
|
+
* - `?` matches exactly one character.
|
|
73
|
+
* - `[abc]` / `[a-z]` matches a single character in the set / range.
|
|
74
|
+
* - `{a,b,c}` matches one of the alternatives.
|
|
75
|
+
*
|
|
76
|
+
* Because matching is done on the **normalized** path (which always starts
|
|
77
|
+
* with `/`), patterns that should match anywhere in the tree need a
|
|
78
|
+
* leading `**/`, e.g. `**/*.log` to hide every `.log` file at any depth.
|
|
79
|
+
*
|
|
80
|
+
* Gotcha: `/foo/**` matches descendants of `/foo` but **not** `/foo`
|
|
81
|
+
* itself, because the glob requires a `/` after `foo` before `**` can
|
|
82
|
+
* match. To hide both the directory and its contents, list both prefixes:
|
|
83
|
+
* `newGlobPathFilter("/foo", "/foo/**")`. {@link newPathFilter} doesn't
|
|
84
|
+
* have this problem and may be a better fit for prefix-only hiding.
|
|
85
|
+
*
|
|
86
|
+
* @param pathGlobs Glob patterns whose match means "hide this path". Pass
|
|
87
|
+
* none to hide nothing.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* const filter = newGlobPathFilter("**/*.log", "/.git", "/.git/**");
|
|
92
|
+
* filter("/src/index.ts"); // true
|
|
93
|
+
* filter("/build.log"); // false (matches **/*.log)
|
|
94
|
+
* filter("/.git"); // false (matches /.git)
|
|
95
|
+
* filter("/.git/HEAD"); // false (matches /.git/**)
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export declare function newGlobPathFilter(...pathGlobs: string[]): PathFilter;
|
|
99
|
+
/**
|
|
100
|
+
* `FilesApi` decorator that hides every path the supplied {@link PathFilter}
|
|
101
|
+
* rejects. Hidden paths are treated as if they do not exist:
|
|
102
|
+
*
|
|
103
|
+
* - `read` / `list` yield empty iterables.
|
|
104
|
+
* - `stats` returns `undefined`; `exists` returns `false`.
|
|
105
|
+
* - `remove` returns `false` (no error, nothing changed).
|
|
106
|
+
* - `move` / `copy` return `false` if either endpoint is hidden.
|
|
107
|
+
* - `write` / `mkdir` reject with an `Error` (since silently dropping a
|
|
108
|
+
* write would lose data).
|
|
109
|
+
* - `list` recursively skips entries whose paths are hidden, so iterating a
|
|
110
|
+
* visible parent never reveals a hidden child.
|
|
111
|
+
*
|
|
112
|
+
* Wrap any `FilesApi` to scope its visibility without changing the
|
|
113
|
+
* underlying storage; the wrapped instance still holds the data, it is just
|
|
114
|
+
* not reachable through this decorator.
|
|
115
|
+
*
|
|
116
|
+
* Pair with one of the built-in {@link PathFilter} factories
|
|
117
|
+
* ({@link newPathFilter}, {@link newRegexpPathFilter},
|
|
118
|
+
* {@link newGlobPathFilter}) or pass any predicate of shape
|
|
119
|
+
* `(path) => boolean | Promise<boolean>`.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* import {
|
|
124
|
+
* FilteredFilesApi,
|
|
125
|
+
* newGlobPathFilter,
|
|
126
|
+
* newPathFilter,
|
|
127
|
+
* newRegexpPathFilter,
|
|
128
|
+
* } from "@statewalker/webrun-files-composite";
|
|
129
|
+
*
|
|
130
|
+
* // Hide by path prefix
|
|
131
|
+
* const noVcs = new FilteredFilesApi(sourceFiles, newPathFilter("/.git", "/.cache"));
|
|
132
|
+
* await noVcs.exists("/.git"); // false
|
|
133
|
+
* await noVcs.write("/.git/x", data); // throws "Path is hidden"
|
|
134
|
+
*
|
|
135
|
+
* // Hide by regexp
|
|
136
|
+
* const noLogs = new FilteredFilesApi(sourceFiles, newRegexpPathFilter(/\.log$/));
|
|
137
|
+
*
|
|
138
|
+
* // Hide by glob (extended + globstar mode)
|
|
139
|
+
* const noJunk = new FilteredFilesApi(
|
|
140
|
+
* sourceFiles,
|
|
141
|
+
* newGlobPathFilter("**/*.log", "/.git", "/.git/**"),
|
|
142
|
+
* );
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export declare class FilteredFilesApi implements FilesApi {
|
|
146
|
+
private readonly source;
|
|
147
|
+
private readonly pathFilter;
|
|
148
|
+
/**
|
|
149
|
+
* @param source The underlying `FilesApi` whose paths will be selectively
|
|
150
|
+
* hidden. Operations always delegate to this instance; the decorator
|
|
151
|
+
* only adds the visibility check.
|
|
152
|
+
* @param pathFilter Predicate that decides per-call whether a normalized
|
|
153
|
+
* path is visible. See {@link PathFilter}.
|
|
154
|
+
*/
|
|
155
|
+
constructor(source: FilesApi, pathFilter: PathFilter);
|
|
156
|
+
protected isHidden(path: string): Promise<boolean>;
|
|
157
|
+
read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array>;
|
|
158
|
+
write(path: string, content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>): Promise<void>;
|
|
159
|
+
mkdir(path: string): Promise<void>;
|
|
160
|
+
list(path: string, options?: ListOptions): AsyncIterable<FileInfo>;
|
|
161
|
+
stats(path: string): Promise<FileStats | undefined>;
|
|
162
|
+
exists(path: string): Promise<boolean>;
|
|
163
|
+
remove(path: string): Promise<boolean>;
|
|
164
|
+
move(source: string, target: string): Promise<boolean>;
|
|
165
|
+
copy(source: string, target: string): Promise<boolean>;
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=filtered-files-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filtered-files-api.d.ts","sourceRoot":"","sources":["../src/filtered-files-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAInC;;;;;;;;;;GAUG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,aAAa,CAAC,GAAG,YAAY,EAAE,MAAM,EAAE,GAAG,UAAU,CAUnE;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,UAAU,CAQxE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,SAAS,EAAE,MAAM,EAAE,GAAG,UAAU,CAGpE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,qBAAa,gBAAiB,YAAW,QAAQ;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IAExC;;;;;;OAMG;gBACS,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU;cAKpC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIjD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC;IAKrE,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC;IAOV,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOjC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC;IAQnE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAKnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKtC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKtC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAOtD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAM7D"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob → RegExp compiler.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript port of `glob-to-regexp` by Nick Fitzgerald.
|
|
5
|
+
*
|
|
6
|
+
* Upstream:
|
|
7
|
+
* - https://github.com/fitzgen/glob-to-regexp
|
|
8
|
+
* - source: https://raw.githubusercontent.com/fitzgen/glob-to-regexp/master/index.js
|
|
9
|
+
* - tests: https://raw.githubusercontent.com/fitzgen/glob-to-regexp/master/test.js
|
|
10
|
+
*
|
|
11
|
+
* The original is published under the BSD 2-Clause license — see the
|
|
12
|
+
* upstream repository for the full text. This port preserves the original
|
|
13
|
+
* semantics; only the surface API has been re-typed for TypeScript.
|
|
14
|
+
*/
|
|
15
|
+
export interface GlobToRegExpOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Enable bash-style extended globs. When `true`:
|
|
18
|
+
*
|
|
19
|
+
* - `?` matches exactly one character.
|
|
20
|
+
* - `[abc]` / `[a-z]` matches a single character in the set / range.
|
|
21
|
+
* - `{foo,bar}` matches one of the alternatives.
|
|
22
|
+
*
|
|
23
|
+
* When `false` (default), each of these characters is treated literally
|
|
24
|
+
* (they are escaped in the output regexp).
|
|
25
|
+
*/
|
|
26
|
+
extended?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Enable bash-style globstar semantics for `*` and `**`.
|
|
29
|
+
*
|
|
30
|
+
* - With `globstar: false` (default), every run of `*`s is translated to
|
|
31
|
+
* `.*` — so `*` matches any number of characters, including `/`.
|
|
32
|
+
* - With `globstar: true`, a single `*` only matches within one path
|
|
33
|
+
* segment (`[^/]*`), and a `**` segment (`**` between `/`s, or at the
|
|
34
|
+
* start/end of the pattern) matches zero or more whole segments.
|
|
35
|
+
*/
|
|
36
|
+
globstar?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* RegExp flags passed to the `RegExp` constructor. When `flags` includes
|
|
39
|
+
* `"g"`, the produced regexp is **not** anchored with `^…$`, so the glob
|
|
40
|
+
* matches anywhere in the string instead of the whole string.
|
|
41
|
+
*/
|
|
42
|
+
flags?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Compiles a glob pattern into a `RegExp`.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* globToRegExp("*.js"); // /^.*\.js$/
|
|
50
|
+
* globToRegExp("*.js", { globstar: true }); // /^([^/]*)\.js$/
|
|
51
|
+
* globToRegExp("/foo/**", { globstar: true }) // /^\/foo\/((?:[^/]*(?:\/|$))*)$/
|
|
52
|
+
* globToRegExp("foo{bar,baz}", { extended: true }); // /^foo(bar|baz)$/
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function globToRegExp(glob: string, opts?: GlobToRegExpOptions): RegExp;
|
|
56
|
+
//# sourceMappingURL=glob-to-regexp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glob-to-regexp.d.ts","sourceRoot":"","sources":["../src/glob-to-regexp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,mBAAwB,GAAG,MAAM,CA2HjF"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { FileInfo, FileStats, FilesApi, ListOptions, ReadOptions } from "@statewalker/webrun-files";
|
|
2
|
+
import type { FileGuard } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* `FilesApi` decorator that runs every call through an ordered list of
|
|
5
|
+
* {@link FileGuard}s. A guard fires when its `operations` set intersects the
|
|
6
|
+
* effective operation(s) for the current call. The first guard whose
|
|
7
|
+
* `check` returns `false` aborts the call by throwing an `Error` with that
|
|
8
|
+
* guard's `message` (defaulting to `"Access denied"`) followed by the
|
|
9
|
+
* normalized path.
|
|
10
|
+
*
|
|
11
|
+
* Effective operations per call:
|
|
12
|
+
*
|
|
13
|
+
* | Method | Operations checked |
|
|
14
|
+
* | ------------- | --------------------------------------------------- |
|
|
15
|
+
* | `read` | `read` |
|
|
16
|
+
* | `write` | `write` |
|
|
17
|
+
* | `mkdir` | `mkdir` |
|
|
18
|
+
* | `remove` | `remove` |
|
|
19
|
+
* | `list` | `list` on the path AND on each directory entry |
|
|
20
|
+
* | `stats` | `list` (a stat reveals existence like a tiny list) |
|
|
21
|
+
* | `exists` | `read` (existence is a read of metadata) |
|
|
22
|
+
* | `move(s, t)` | `move`+`read` on source; `move`+`write` on target |
|
|
23
|
+
* | `copy(s, t)` | `copy`+`read` on source; `copy`+`write` on target |
|
|
24
|
+
*
|
|
25
|
+
* The expanded checks for `move`/`copy` mean a guard that blocks `read` on
|
|
26
|
+
* a path also prevents move/copy *from* that path, and a `write`-blocking
|
|
27
|
+
* guard prevents move/copy *to* it. Likewise, an `exists` call respects any
|
|
28
|
+
* read guard, and `stats` respects any list guard.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const api = new GuardedFilesApi(source, [
|
|
33
|
+
* {
|
|
34
|
+
* operations: ["write", "remove", "move", "mkdir"],
|
|
35
|
+
* check: (p) => !p.startsWith("/.system/"),
|
|
36
|
+
* message: "system folder is read-only",
|
|
37
|
+
* },
|
|
38
|
+
* ]);
|
|
39
|
+
* await api.write("/.system/cfg", data); // throws "system folder is read-only: /.system/cfg"
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare class GuardedFilesApi implements FilesApi {
|
|
43
|
+
private readonly source;
|
|
44
|
+
private readonly guards;
|
|
45
|
+
/**
|
|
46
|
+
* @param source The underlying `FilesApi` whose calls will be policed.
|
|
47
|
+
* Allowed operations delegate straight through.
|
|
48
|
+
* @param guards Ordered list of access policies. The wrapper takes a
|
|
49
|
+
* defensive copy, so mutating the array afterwards has no effect.
|
|
50
|
+
* An empty list disables every check (the wrapper becomes a passthrough).
|
|
51
|
+
*/
|
|
52
|
+
constructor(source: FilesApi, guards: FileGuard[]);
|
|
53
|
+
private checkGuard;
|
|
54
|
+
read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array>;
|
|
55
|
+
write(path: string, content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>): Promise<void>;
|
|
56
|
+
mkdir(path: string): Promise<void>;
|
|
57
|
+
list(path: string, options?: ListOptions): AsyncIterable<FileInfo>;
|
|
58
|
+
stats(path: string): Promise<FileStats | undefined>;
|
|
59
|
+
exists(path: string): Promise<boolean>;
|
|
60
|
+
remove(path: string): Promise<boolean>;
|
|
61
|
+
move(source: string, target: string): Promise<boolean>;
|
|
62
|
+
copy(source: string, target: string): Promise<boolean>;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=guarded-files-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guarded-files-api.d.ts","sourceRoot":"","sources":["../src/guarded-files-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,SAAS,EAAiB,MAAM,YAAY,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,qBAAa,eAAgB,YAAW,QAAQ;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IAErC;;;;;;OAMG;gBACS,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE;IAKjD,OAAO,CAAC,UAAU;IAWlB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC;IAK9D,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC;IAKV,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC;IAUzE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAKnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKhC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKtC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAMtD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAK7D"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export { CompositeFilesApi } from "./composite-files-api.js";
|
|
2
|
+
export type { PathFilter } from "./filtered-files-api.js";
|
|
3
|
+
export { FilteredFilesApi, newGlobPathFilter, newPathFilter, newRegexpPathFilter, } from "./filtered-files-api.js";
|
|
4
|
+
export type { GlobToRegExpOptions } from "./glob-to-regexp.js";
|
|
5
|
+
export { globToRegExp } from "./glob-to-regexp.js";
|
|
6
|
+
export { GuardedFilesApi } from "./guarded-files-api.js";
|
|
2
7
|
export type { FileGuard, FileOperation } from "./types.js";
|
|
3
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,mBAAmB,GACpB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,10 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem operations a {@link FileGuard} can intercept.
|
|
3
|
+
*
|
|
4
|
+
* - `read` — reading file content (`FilesApi.read`). Also implicitly checked
|
|
5
|
+
* on the source side of `move`/`copy`, and on every `exists` call.
|
|
6
|
+
* - `write` — writing file content (`FilesApi.write`). Also implicitly
|
|
7
|
+
* checked on the target side of `move`/`copy`.
|
|
8
|
+
* - `mkdir` — creating directories (`FilesApi.mkdir`).
|
|
9
|
+
* - `list` — listing directory contents (`FilesApi.list`). Also implicitly
|
|
10
|
+
* checked on every `stats` call and on each directory entry encountered
|
|
11
|
+
* while iterating a listing.
|
|
12
|
+
* - `remove` — deleting files or directories (`FilesApi.remove`).
|
|
13
|
+
* - `move` — relocating a file or directory (`FilesApi.move`).
|
|
14
|
+
* - `copy` — copying a file or directory (`FilesApi.copy`).
|
|
15
|
+
*
|
|
16
|
+
* Only the operations listed in a guard's {@link FileGuard.operations} array
|
|
17
|
+
* trigger that guard's predicate.
|
|
18
|
+
*/
|
|
1
19
|
export type FileOperation = "read" | "write" | "remove" | "move" | "copy" | "list" | "mkdir";
|
|
20
|
+
/**
|
|
21
|
+
* Per-operation, per-path access policy applied by `GuardedFilesApi`.
|
|
22
|
+
*
|
|
23
|
+
* Guards are evaluated in the order they are passed to the wrapper, and the
|
|
24
|
+
* first guard whose `check` returns `false` for a matching operation aborts
|
|
25
|
+
* the call by throwing an `Error` carrying its `message`.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const guard: FileGuard = {
|
|
30
|
+
* operations: ["write", "remove", "move"],
|
|
31
|
+
* check: (path) => !path.startsWith("/.system/"),
|
|
32
|
+
* message: "system folder is read-only",
|
|
33
|
+
* };
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
2
36
|
export interface FileGuard {
|
|
3
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Filesystem operations this guard applies to. The guard's `check` is
|
|
39
|
+
* invoked only for calls whose effective operation set intersects this
|
|
40
|
+
* list (e.g. a guard listing `"read"` also fires on `move`/`copy` source
|
|
41
|
+
* paths and on every `exists` call).
|
|
42
|
+
*/
|
|
4
43
|
operations: FileOperation[];
|
|
5
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Predicate invoked with the **normalized** path (single leading slash, no
|
|
46
|
+
* trailing slash, collapsed `.` segments and double slashes). Returning
|
|
47
|
+
* `true` allows the operation; returning `false` denies it and the wrapper
|
|
48
|
+
* throws an `Error` with this guard's {@link message}.
|
|
49
|
+
*/
|
|
6
50
|
check: (path: string) => boolean;
|
|
7
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Optional message used as the prefix of the thrown `Error`. The wrapper
|
|
53
|
+
* appends `: <normalized-path>`. Defaults to `"Access denied"`.
|
|
54
|
+
*/
|
|
8
55
|
message?: string;
|
|
9
56
|
}
|
|
10
57
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7F,MAAM,WAAW,SAAS;IACxB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7F;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,SAAS;IACxB;;;;;OAKG;IACH,UAAU,EAAE,aAAa,EAAE,CAAC;IAE5B;;;;;OAKG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAEjC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
|
package/package.json
CHANGED
|
@@ -6,7 +6,6 @@ import type {
|
|
|
6
6
|
ReadOptions,
|
|
7
7
|
} from "@statewalker/webrun-files";
|
|
8
8
|
import { joinPath, normalizePath } from "@statewalker/webrun-files";
|
|
9
|
-
import type { FileGuard, FileOperation } from "./types.js";
|
|
10
9
|
|
|
11
10
|
interface MountEntry {
|
|
12
11
|
prefix: string;
|
|
@@ -14,14 +13,63 @@ interface MountEntry {
|
|
|
14
13
|
basePath: string;
|
|
15
14
|
}
|
|
16
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Composite `FilesApi` that routes calls to one of several backends based
|
|
18
|
+
* on a path prefix. Mounts are matched by **longest prefix wins**, so a
|
|
19
|
+
* mount at `/a/b` takes precedence over a mount at `/a` for paths under
|
|
20
|
+
* `/a/b/...`. The mount point itself appears in listings as a synthetic
|
|
21
|
+
* directory and cannot be removed.
|
|
22
|
+
*
|
|
23
|
+
* Each backend can use a sub-directory of its own filesystem as the mount
|
|
24
|
+
* root via `fsPath` (constructor `rootPath` for the implicit root mount,
|
|
25
|
+
* `fsPath` argument for additional mounts). Cross-mount `move` is
|
|
26
|
+
* implemented as copy-then-delete; there is no atomicity guarantee.
|
|
27
|
+
*
|
|
28
|
+
* Access control and visibility filtering are intentionally **not** part of
|
|
29
|
+
* this class — wrap with {@link GuardedFilesApi} or {@link FilteredFilesApi}
|
|
30
|
+
* (or both) instead.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const fs = new CompositeFilesApi(localFs, "/projects")
|
|
35
|
+
* .mount("/docs", s3Fs, "/documentation")
|
|
36
|
+
* .mount("/cache", memFs);
|
|
37
|
+
* await fs.write("/readme.md", data); // → localFs:/projects/readme.md
|
|
38
|
+
* await fs.write("/docs/api.md", data); // → s3Fs:/documentation/api.md
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
17
41
|
export class CompositeFilesApi implements FilesApi {
|
|
18
42
|
private mounts: MountEntry[];
|
|
19
|
-
private guards: FileGuard[] = [];
|
|
20
43
|
|
|
44
|
+
/**
|
|
45
|
+
* @param root Default backend used for any path that does not match a
|
|
46
|
+
* more specific mount. All paths are routed here unless `mount()`
|
|
47
|
+
* intercepts them.
|
|
48
|
+
* @param rootPath Optional sub-directory of the root backend to use as
|
|
49
|
+
* the composite filesystem's `/`. For example, `rootPath = "/projects"`
|
|
50
|
+
* makes the composite path `/readme.md` resolve to `/projects/readme.md`
|
|
51
|
+
* in the root backend. Defaults to `"/"` (no remapping).
|
|
52
|
+
*/
|
|
21
53
|
constructor(root: FilesApi, rootPath?: string) {
|
|
22
54
|
this.mounts = [{ prefix: "/", api: root, basePath: normalizePath(rootPath ?? "/") }];
|
|
23
55
|
}
|
|
24
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Attaches a backend to handle every composite path under `path`. The
|
|
59
|
+
* mount prefix is normalized; paths under it are resolved against the
|
|
60
|
+
* mount's `fsPath` sub-directory (defaulting to `"/"`).
|
|
61
|
+
*
|
|
62
|
+
* @param path Composite-namespace prefix (e.g. `"/docs"`). Mounting at
|
|
63
|
+
* `"/"` is forbidden — use the constructor `root` argument instead.
|
|
64
|
+
* @param api The backend `FilesApi` to delegate to for paths under
|
|
65
|
+
* `path`. Wrap it in {@link FilteredFilesApi} / {@link GuardedFilesApi}
|
|
66
|
+
* first if you want mount-local filtering or guards.
|
|
67
|
+
* @param fsPath Sub-directory of the mounted backend used as its mount
|
|
68
|
+
* root, e.g. `mount("/docs", s3, "/documentation")` makes
|
|
69
|
+
* `/docs/api.md` resolve to `/documentation/api.md` on `s3`.
|
|
70
|
+
* @returns `this`, for chaining.
|
|
71
|
+
* @throws If `path` normalizes to `"/"`.
|
|
72
|
+
*/
|
|
25
73
|
mount(path: string, api: FilesApi, fsPath?: string): this {
|
|
26
74
|
const prefix = normalizePath(path);
|
|
27
75
|
if (prefix === "/") {
|
|
@@ -33,11 +81,6 @@ export class CompositeFilesApi implements FilesApi {
|
|
|
33
81
|
return this;
|
|
34
82
|
}
|
|
35
83
|
|
|
36
|
-
guard(operations: FileOperation[], check: (path: string) => boolean, message?: string): this {
|
|
37
|
-
this.guards.push({ operations, check, message });
|
|
38
|
-
return this;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
84
|
// --- Mount resolution ---
|
|
42
85
|
|
|
43
86
|
private resolve(path: string): { api: FilesApi; resolvedPath: string } {
|
|
@@ -78,23 +121,9 @@ export class CompositeFilesApi implements FilesApi {
|
|
|
78
121
|
return result;
|
|
79
122
|
}
|
|
80
123
|
|
|
81
|
-
// --- Guard checking ---
|
|
82
|
-
|
|
83
|
-
private checkGuard(operation: FileOperation, path: string): void {
|
|
84
|
-
const normalized = normalizePath(path);
|
|
85
|
-
for (const guard of this.guards) {
|
|
86
|
-
if (!guard.operations.includes(operation)) continue;
|
|
87
|
-
if (!guard.check(normalized)) {
|
|
88
|
-
const msg = guard.message ?? "Access denied";
|
|
89
|
-
throw new Error(`${msg}: ${normalized}`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
124
|
// --- FilesApi implementation ---
|
|
95
125
|
|
|
96
126
|
read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array> {
|
|
97
|
-
this.checkGuard("read", path);
|
|
98
127
|
const { api, resolvedPath } = this.resolve(path);
|
|
99
128
|
return api.read(resolvedPath, options);
|
|
100
129
|
}
|
|
@@ -103,19 +132,16 @@ export class CompositeFilesApi implements FilesApi {
|
|
|
103
132
|
path: string,
|
|
104
133
|
content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,
|
|
105
134
|
): Promise<void> {
|
|
106
|
-
this.checkGuard("write", path);
|
|
107
135
|
const { api, resolvedPath } = this.resolve(path);
|
|
108
136
|
return api.write(resolvedPath, content);
|
|
109
137
|
}
|
|
110
138
|
|
|
111
139
|
async mkdir(path: string): Promise<void> {
|
|
112
|
-
this.checkGuard("mkdir", path);
|
|
113
140
|
const { api, resolvedPath } = this.resolve(path);
|
|
114
141
|
return api.mkdir(resolvedPath);
|
|
115
142
|
}
|
|
116
143
|
|
|
117
144
|
async *list(path: string, options?: ListOptions): AsyncIterable<FileInfo> {
|
|
118
|
-
this.checkGuard("list", path);
|
|
119
145
|
const normalized = normalizePath(path);
|
|
120
146
|
const { api, resolvedPath } = this.resolve(path);
|
|
121
147
|
|
|
@@ -184,14 +210,11 @@ export class CompositeFilesApi implements FilesApi {
|
|
|
184
210
|
if (this.isMountPoint(normalized)) {
|
|
185
211
|
throw new Error(`Cannot remove mount point: ${normalized}`);
|
|
186
212
|
}
|
|
187
|
-
this.checkGuard("remove", path);
|
|
188
213
|
const { api, resolvedPath } = this.resolve(path);
|
|
189
214
|
return api.remove(resolvedPath);
|
|
190
215
|
}
|
|
191
216
|
|
|
192
217
|
async move(source: string, target: string): Promise<boolean> {
|
|
193
|
-
this.checkGuard("move", source);
|
|
194
|
-
this.checkGuard("move", target);
|
|
195
218
|
const src = this.resolve(source);
|
|
196
219
|
const tgt = this.resolve(target);
|
|
197
220
|
|
|
@@ -209,8 +232,6 @@ export class CompositeFilesApi implements FilesApi {
|
|
|
209
232
|
}
|
|
210
233
|
|
|
211
234
|
async copy(source: string, target: string): Promise<boolean> {
|
|
212
|
-
this.checkGuard("copy", source);
|
|
213
|
-
this.checkGuard("copy", target);
|
|
214
235
|
const src = this.resolve(source);
|
|
215
236
|
const tgt = this.resolve(target);
|
|
216
237
|
|