@statewalker/file-mounts.core 0.0.0-stage → 0.1.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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +316 -2
  3. package/dist/changes.d.ts +16 -0
  4. package/dist/changes.d.ts.map +1 -0
  5. package/dist/changes.js +18 -0
  6. package/dist/changes.js.map +1 -0
  7. package/dist/connection-test.d.ts +31 -0
  8. package/dist/connection-test.d.ts.map +1 -0
  9. package/dist/connection-test.js +152 -0
  10. package/dist/connection-test.js.map +1 -0
  11. package/dist/index.d.ts +20 -0
  12. package/dist/index.js +20 -0
  13. package/dist/key-value-store.d.ts +30 -0
  14. package/dist/key-value-store.d.ts.map +1 -0
  15. package/dist/key-value-store.js +55 -0
  16. package/dist/key-value-store.js.map +1 -0
  17. package/dist/local-folder-mount-type.d.ts +13 -0
  18. package/dist/local-folder-mount-type.d.ts.map +1 -0
  19. package/dist/local-folder-mount-type.js +64 -0
  20. package/dist/local-folder-mount-type.js.map +1 -0
  21. package/dist/main-storage.d.ts +8 -0
  22. package/dist/main-storage.d.ts.map +1 -0
  23. package/dist/main-storage.js +20 -0
  24. package/dist/main-storage.js.map +1 -0
  25. package/dist/memory-mount-type.d.ts +6 -0
  26. package/dist/memory-mount-type.d.ts.map +1 -0
  27. package/dist/memory-mount-type.js +24 -0
  28. package/dist/memory-mount-type.js.map +1 -0
  29. package/dist/mount-config.d.ts +57 -0
  30. package/dist/mount-config.d.ts.map +1 -0
  31. package/dist/mount-config.js +119 -0
  32. package/dist/mount-config.js.map +1 -0
  33. package/dist/mount-settings-file.d.ts +16 -0
  34. package/dist/mount-settings-file.d.ts.map +1 -0
  35. package/dist/mount-settings-file.js +72 -0
  36. package/dist/mount-settings-file.js.map +1 -0
  37. package/dist/mount-table.d.ts +50 -0
  38. package/dist/mount-table.d.ts.map +1 -0
  39. package/dist/mount-table.js +203 -0
  40. package/dist/mount-table.js.map +1 -0
  41. package/dist/mount-type.d.ts +45 -0
  42. package/dist/mount-type.d.ts.map +1 -0
  43. package/dist/mount-type.js +12 -0
  44. package/dist/mount-type.js.map +1 -0
  45. package/dist/opfs-mount-type.d.ts +6 -0
  46. package/dist/opfs-mount-type.d.ts.map +1 -0
  47. package/dist/opfs-mount-type.js +22 -0
  48. package/dist/opfs-mount-type.js.map +1 -0
  49. package/dist/passkeys.d.ts +55 -0
  50. package/dist/passkeys.d.ts.map +1 -0
  51. package/dist/passkeys.js +155 -0
  52. package/dist/passkeys.js.map +1 -0
  53. package/dist/problem.d.ts +21 -0
  54. package/dist/problem.d.ts.map +1 -0
  55. package/dist/problem.js +23 -0
  56. package/dist/problem.js.map +1 -0
  57. package/dist/profiles.d.ts +37 -0
  58. package/dist/profiles.d.ts.map +1 -0
  59. package/dist/profiles.js +115 -0
  60. package/dist/profiles.js.map +1 -0
  61. package/dist/project-archive.d.ts +16 -0
  62. package/dist/project-archive.d.ts.map +1 -0
  63. package/dist/project-archive.js +47 -0
  64. package/dist/project-archive.js.map +1 -0
  65. package/dist/s3-problems.d.ts +22 -0
  66. package/dist/s3-problems.d.ts.map +1 -0
  67. package/dist/s3-problems.js +90 -0
  68. package/dist/s3-problems.js.map +1 -0
  69. package/dist/s3.d.ts +33 -0
  70. package/dist/s3.d.ts.map +1 -0
  71. package/dist/s3.js +214 -0
  72. package/dist/s3.js.map +1 -0
  73. package/dist/secret-vault.d.ts +81 -0
  74. package/dist/secret-vault.d.ts.map +1 -0
  75. package/dist/secret-vault.js +214 -0
  76. package/dist/secret-vault.js.map +1 -0
  77. package/dist/unavailable-files.d.ts +7 -0
  78. package/dist/unavailable-files.d.ts.map +1 -0
  79. package/dist/unavailable-files.js +27 -0
  80. package/dist/unavailable-files.js.map +1 -0
  81. package/dist/vault-records.d.ts +31 -0
  82. package/dist/vault-records.d.ts.map +1 -0
  83. package/dist/vault-records.js +124 -0
  84. package/dist/vault-records.js.map +1 -0
  85. package/package.json +51 -3
  86. package/src/changes.ts +28 -0
  87. package/src/connection-test.ts +160 -0
  88. package/src/index.ts +19 -0
  89. package/src/key-value-store.ts +61 -0
  90. package/src/local-folder-mount-type.ts +75 -0
  91. package/src/main-storage.ts +18 -0
  92. package/src/memory-mount-type.ts +22 -0
  93. package/src/mount-config.ts +173 -0
  94. package/src/mount-settings-file.ts +81 -0
  95. package/src/mount-table.ts +228 -0
  96. package/src/mount-type.ts +41 -0
  97. package/src/opfs-mount-type.ts +21 -0
  98. package/src/passkeys.ts +167 -0
  99. package/src/problem.ts +36 -0
  100. package/src/profiles.ts +157 -0
  101. package/src/project-archive.ts +64 -0
  102. package/src/s3-problems.ts +100 -0
  103. package/src/s3.ts +224 -0
  104. package/src/secret-vault.ts +294 -0
  105. package/src/unavailable-files.ts +26 -0
  106. package/src/vault-records.ts +188 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) {{year}} Mikhail Kotelnikov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,317 @@
1
- # Temporary Holding Version
1
+ # @statewalker/file-mounts.core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ## What it is
4
+
5
+ Storages mounted side by side as one `FilesApi`: a folder of the computer, the browser's private
6
+ storage, an S3 bucket, or memory. Each storage appears at `/<key>` under a read-only root that lists
7
+ them. The package also holds the profiles, each with an encrypted vault for its S3 keys and mounts
8
+ (`/.settings/mounts.json` in a project is only the project's archive copy of the mounts), and a
9
+ five-step S3 connection test that says what is wrong with a bucket instead of "failed to fetch".
10
+ It is React-free; a view layer reads the table's `list()` and `onUpdate`.
11
+
12
+ ## Why it exists
13
+
14
+ A files app talks to several storages at once and must keep working when one of them does not. Three
15
+ things follow from that:
16
+
17
+ - **A broken mount stays in the table.** An unusable storage (locked vault, folder access to be given
18
+ again, wrong keys, no answer) is mounted anyway, with a status. Every operation on it raises
19
+ `MountUnavailable` carrying the reason. An empty folder would look like data loss and invite
20
+ the person to "fix" it by saving over it.
21
+ - **S3 is called straight from the browser.** The bucket needs a CORS rule naming the page's address,
22
+ and the browser reports a missing rule exactly like a dead host. The connection test tells them apart
23
+ and shows the rule to paste.
24
+ - **The person's keys and storages must not sit in plain text.** Each profile's data lives in one encrypted IndexedDB record, unlocked by a password (or a passkey).
25
+
26
+ ## How to use
27
+
28
+ ```ts
29
+ import {
30
+ FilesMountTable, IdbStore, Profiles, firstProfileData, localFolderMountType,
31
+ memoryMountType, opfsMountType, s3MountType,
32
+ } from "@statewalker/file-mounts.core";
33
+
34
+ const records = new IdbStore<unknown>("statewalker-files");
35
+ const profiles = new Profiles({ records });
36
+ const [first] = await profiles.load();
37
+ const vault = first
38
+ ? await profiles.open(first.id) // unlocked when remembered; else vault.unlock(password, remember)
39
+ : await profiles.create("Personal", password, true, firstProfileData(opfsMountType().isAvailable().ok));
40
+
41
+ const table = new FilesMountTable({
42
+ types: [
43
+ opfsMountType(),
44
+ localFolderMountType(new IdbStore("folder-handles")),
45
+ s3MountType({ origin: location.origin }),
46
+ memoryMountType(),
47
+ ],
48
+ secrets: vault,
49
+ });
50
+ await table.apply((await vault.read()).mounts); // creates the mounts, in parallel
51
+ table.files; // one FilesApi: /<key>/... for every mount
52
+ ```
53
+
54
+ After the person unlocks the vault, call `table.apply((await vault.read()).mounts)` again: mounts that were `locked` are
55
+ created, mounts that are already `mounted` are kept. `apply` returns the `FileChange[]` of the mount
56
+ roots it added, replaced or removed; nothing else announces them, so pass the list to the change hub,
57
+ or open panels keep showing "Locked" until they are navigated again:
58
+
59
+ ```ts
60
+ import { newFileChanges } from "@statewalker/file-mounts.core";
61
+
62
+ const changes = newFileChanges(); // the hub panels subscribe to
63
+ if (await vault.unlock(password, remember)) changes.emit(await table.apply((await vault.read()).mounts));
64
+ ```
65
+
66
+ `table.reconnect(key)` returns only the status: after it, emit an `updated` change for `/<key>`.
67
+ Inside a click, `table.apply(mounts, { interactive: true })` or `table.reconnect(key)` may show a
68
+ permission prompt.
69
+
70
+ `vault.lock()` does not unmount S3 storages that are already mounted: their client holds the keys
71
+ until the next `apply` that recreates them, or a reload. Likewise, keys changed in the vault for a
72
+ config that did not change are not picked up by `apply` (the mount is kept as is); `reconnect(key)`
73
+ creates it again with the new keys.
74
+
75
+ The main mount's project keeps archive copies of the profile. After each `vault.update(...)` that
76
+ changes mounts or preferences, write them; *Import from this project* merges them back:
77
+
78
+ ```ts
79
+ import { archiveProfile, importProfile, mergeImported } from "@statewalker/file-mounts.core";
80
+
81
+ await archiveProfile(mainFiles, await vault.read()); // mainFiles: the main mount's FilesApi
82
+
83
+ const archive = await importProfile(mainFiles); // show archive.problem when set
84
+ await vault.update((d) => mergeImported(d, archive));
85
+ ```
86
+
87
+ Passkeys unlock a profile beside its password (WebAuthn PRF). A `PasskeyProblem`'s `message` is what to show the person:
88
+
89
+ ```ts
90
+ import { webAuthnPasskeys } from "@statewalker/file-mounts.core";
91
+
92
+ const passkeys = webAuthnPasskeys({ rpName: "Files" });
93
+ if (await passkeys.available()) await profiles.addPasskey(vault, password, passkeys);
94
+ const unlocked = await profiles.unlockWithPasskey(passkeys, remember); // finds the profile itself
95
+ ```
96
+
97
+ ## What each mount type needs
98
+
99
+ | Type | Needs | Notes |
100
+ |---|---|---|
101
+ | `local-folder` | Chromium (Chrome, Edge) | After a page reload the folder needs one click: `table.reconnect(key)` inside the handler. |
102
+ | `opfs` | A browser with `navigator.storage.getDirectory` | Lives in the page's private storage, in the folder `mounts/<key>`. |
103
+ | `s3` | An unlocked vault, a bucket with a CORS rule | Fields: endpoint, region, bucket, prefix, path-style, access key ID, secret access key (the last two go to the vault, as accounts `<key>/accessKeyId` and `<key>/secretAccessKey`). |
104
+ | `memory` | nothing | Gone on reload; used by tests and demos. |
105
+
106
+ A mount saved with `readOnly: true` is wrapped so writes throw; `mounted: false` keeps the settings
107
+ without mounting.
108
+
109
+ The mount key is also where a storage keeps what belongs to it: OPFS data at `mounts/<key>`, S3 keys
110
+ in the vault under `<key>/<field>`. Changing a mount's key therefore shows an empty OPFS storage, and
111
+ an S3 storage that says `Enter the keys for this storage in Settings → Storage.` The old data and keys
112
+ are not deleted; they stay under the old key, unreachable until a mount uses that key again.
113
+
114
+ ## How S3 settings are tested and why the messages are what they are
115
+
116
+ `testConnection` runs five checks in order and stops at the first failure:
117
+
118
+ 1. **The address answers.** A signed `ListObjectsV2` with `MaxKeys: 0` on the bucket; any HTTP answer,
119
+ even a refusal, counts. It is bucket-scoped because the bucket's CORS rule applies to it; a
120
+ service-level `ListBuckets` gets no CORS headers from AWS or R2 and would always look like CORS.
121
+ 2. **The keys are accepted.** The same call: `InvalidAccessKeyId`, `SignatureDoesNotMatch` and
122
+ `RequestTimeTooSkewed` fail here. `AccessDenied` and `NoSuchBucket` still mean the keys were
123
+ recognised; the next checks report those.
124
+ 3. **The bucket exists.** `HeadBucket`; a 403 passes, because listing says whether the bucket may be read.
125
+ 4. **Listing is allowed.** `ListObjectsV2` under the prefix.
126
+ 5. **Writing is allowed.** Puts and deletes a small object `<prefix>.<key>-write-test-<uuid>`. Skipped
127
+ for read-only mounts, so a read-only key is not reported as broken.
128
+
129
+ Each check has a 15 s limit. The time is the point at which a person stops believing the page is
130
+ working; a longer wait looks like a hang.
131
+
132
+ A network failure is followed by an unsigned `no-cors` request to the endpoint. If that answers, the
133
+ host is up and the browser blocked the signed call: the problem is `cors`, and the report carries the
134
+ rule for the page's origin. If it does not answer, the problem is `unreachable`. Without this second
135
+ request the two cases are indistinguishable in a browser.
136
+
137
+ When the region is left empty, which the "Other" and R2 presets allow, the client signs with
138
+ `us-east-1`, which is what S3-compatible servers expect by default. The AWS, MinIO / RustFS, B2 and
139
+ Wasabi presets require a region in the form, so those are never signed with a guessed one.
140
+
141
+ Messages, as the person sees them:
142
+
143
+ | Situation | Message |
144
+ |---|---|
145
+ | Wrong keys | `Wrong access key or secret.` |
146
+ | Clock off | `This computer's clock is off by more than 15 minutes; S3 refuses the request.` |
147
+ | No such bucket | ``Bucket `name` not found at this endpoint.`` |
148
+ | Not allowed to list / write | `These keys can't list this bucket.` / `These keys can't write to this bucket.` |
149
+ | CORS rule missing | `The bucket doesn't allow this site. Add this CORS rule to the bucket:` (followed by the rule) |
150
+ | Host dead | ``No answer from `endpoint`.`` |
151
+ | Hang | `No answer in 15 seconds.` |
152
+ | Vault locked | `Locked: unlock to use the S3 storages.` |
153
+ | S3 keys not entered, or no vault created yet | `Enter the keys for this storage in Settings → Storage.` |
154
+ | Folder access lost | ``Click Reconnect to give access to `folder` again.`` |
155
+ | Browser without folders | `This browser can't open local folders; use Chrome or Edge.` |
156
+
157
+ When an S3 mount is created, one listing runs at once, so wrong keys, a missing bucket or CORS show as
158
+ the mount's status and not as an empty folder later.
159
+
160
+ ## Examples
161
+
162
+ Test settings before saving them:
163
+
164
+ ```ts
165
+ import { testConnection } from "@statewalker/file-mounts.core";
166
+
167
+ const report = await testConnection(
168
+ { key: "archive", name: "Archive", type: "s3", config: { endpoint: "http://127.0.0.1:9000", bucket: "files" } },
169
+ { accessKeyId: "...", secretAccessKey: "..." },
170
+ { origin: location.origin },
171
+ );
172
+ if (!report.ok) console.log(report.problem?.message, report.problem?.fix?.json);
173
+ ```
174
+
175
+ Read a mount that cannot be used:
176
+
177
+ ```ts
178
+ import { MountUnavailable } from "@statewalker/file-mounts.core";
179
+
180
+ try {
181
+ for await (const _ of table.files.list("/archive")) break;
182
+ } catch (e) {
183
+ if (e instanceof MountUnavailable) console.log(e.problem.cause, e.problem.message);
184
+ }
185
+ ```
186
+
187
+ Store a secret and change the password:
188
+
189
+ ```ts
190
+ const vault = await profiles.create("Work", "correct horse", true); // remember: silent unlock after a reload
191
+ await vault.set("archive/accessKeyId", "AKIA...");
192
+ await vault.changePassword("correct horse", "battery staple"); // false when the old one is wrong
193
+ ```
194
+
195
+ ## Internals
196
+
197
+ ### How the vault protects the keys
198
+
199
+ Each profile has one record, `vault:<id>`, in a single `KeyValueStore` (IndexedDB). It holds a sealed
200
+ `ProfileData` (secrets, mounts, the main storage, preferences) and the wrapped data key. A random
201
+ AES-GCM-256 data key seals the data; a key derived from the password with PBKDF2-SHA256 wraps the data
202
+ key. A password change re-wraps the one data key and leaves the sealed data alone. Accounts are named
203
+ `<mount key>/<field>`. The profile id is bound into both ciphertexts as additional data, so a record
204
+ copied under another profile does not decrypt. Kept in clear are the profile list (`profiles`: ids
205
+ and names) and, in each vault record, the passkey credential ids and creation times and the KDF
206
+ parameters (salt, iterations). None of these is a secret.
207
+
208
+ The default is 600 000 PBKDF2 iterations: the current OWASP guidance for SHA-256, and a fraction of a
209
+ second on a laptop. Fewer would make a stolen record cheaper to guess against; more makes every unlock
210
+ slow on a phone. Tests pass `iterations: 1000`.
211
+
212
+ *Remember* stores the **data key**, non-extractable, at `remembered:<id>`, so the next open unlocks
213
+ silently. It is the data key and not the password-derived key because it survives a password change
214
+ and works after any kind of unlock; and whoever can read this browser's IndexedDB could use a
215
+ remembered password key just as well. Writes take a Web Lock per profile and each write re-reads the
216
+ record, so two tabs changing different things keep both.
217
+
218
+ A wrong password never changes anything: `unlock` and `changePassword` return `false` and the state
219
+ stays as it was. Both check the password against the record as it is now, so after another tab changed
220
+ the password, the new one works in this tab too, without a reload.
221
+
222
+ A lost password cannot be recovered. `profiles.remove(id)` deletes the vault and its remembered key; the
223
+ files in the storages are not touched, but the mounts and keys must be entered again. Using a removed
224
+ profile's vault fails with `This profile was removed.`
225
+
226
+ ### A passkey unlocks the vault through PRF, and the password stays
227
+
228
+ The authenticator returns the same 32-byte PRF secret for this site and the fixed input
229
+ `statewalker-files vault v1`, after verifying the person. HKDF-SHA256 with a random salt per wrap turns
230
+ that secret into the key that wraps the data key, next to the password wrap. The passkey's user handle
231
+ is the profile id and its user name the profile's name, so one *Unlock with a passkey* finds the
232
+ profile. The password is checked before the passkey prompt, so a wrong one leaves no orphan passkey.
233
+
234
+ Support differs: Chromium with security keys and phones, Safari 18+ with Apple Passwords, Windows Hello
235
+ only on recent builds, Firefox unclear. `available()` asks `getClientCapabilities`. An authenticator
236
+ that registers a passkey without PRF leaves that passkey in its own list though it unlocks nothing;
237
+ adding then fails with "This browser can't use passkeys to unlock; use the password." Removing a passkey
238
+ from the profile does not delete it from the authenticator, which keeps offering it and gets "This
239
+ passkey isn't linked to a profile on this browser." There is no server, so the challenge is random and
240
+ nothing is verified beyond the PRF secret. Keep the password: a deleted passkey would otherwise lock
241
+ the person out.
242
+
243
+ ### How the table keeps working when one storage does not
244
+
245
+ `apply` and `reconnect` run one after another through a queue; mounts of one `apply` are created in
246
+ parallel. `apply` validates every key first and rejects before changing anything. A create that does
247
+ not settle in 15 s (`createTimeoutMs`) is `failed` with `No answer in 15 seconds.`; other mounts are
248
+ unaffected, and the queue moves on. The message names 15 seconds whatever `createTimeoutMs` is set to.
249
+
250
+ The one exception is a type that declares `prompts: true` (only `local-folder`), created
251
+ interactively (`reconnect`, or `apply` with `interactive: true`): it waits on a browser permission
252
+ prompt, which always settles when the person answers, and timing it out would fail a folder while
253
+ they are still reading the prompt. Every other type is timed out even inside a click, because an S3
254
+ host that never answers would otherwise hold the queue, and every later `apply` and `reconnect`,
255
+ forever.
256
+
257
+ ### The main storage is the project, and keeps an archive of the profile
258
+
259
+ `canBeMain(config)` is true for `opfs`, `local-folder` and `memory`, and false for `s3`. The main
260
+ storage is the open project and must open before anything else; an S3 storage is locked until the
261
+ vault opens, so it cannot be the one that opens first. `firstProfileData(opfs)` is what a new profile
262
+ starts with: one "Browser storage" mount as main, or a memory mount where OPFS is missing.
263
+
264
+ `archiveProfile` writes `/.settings/mounts.json` and `/.settings/preferences.json` into the project.
265
+ Secrets never go there: the mounts file holds configs only, and the keys stay in the vault. The vault
266
+ always wins: `importProfile` reads the copies only on an explicit import, and `mergeImported` keeps
267
+ the profile's own mounts and preference values, adding only the mounts with unknown keys and the
268
+ preferences it lacks. A damaged copy brings in what can be read, and `problem` says what was left
269
+ out, for example `1 saved storage could not be read and was left out. The archived preferences are
270
+ damaged; they were left out.`
271
+
272
+ ### A save over a damaged mounts.json first keeps a backup of it
273
+
274
+ `mounts.json` is the project's archive copy of the mounts. `loadMountConfigs` returns the mounts in `/.settings/mounts.json`. A file that is not valid JSON or not a
275
+ list loads as no mounts, with `problem: "The storage settings file is damaged; it is kept as is until you save."`.
276
+ Entries that cannot be read are left out with a problem count, for example
277
+ `2 saved storages could not be read and were left out.` Loading never rewrites the file.
278
+
279
+ The next `saveMountConfigs` writes only the mounts it is given, so the entries left out would be
280
+ gone. Before writing, it reads the file again; if it does not load cleanly (by the same rules), its
281
+ exact text is first copied to `mounts.damaged-<timestamp>.json` in the same folder (for example
282
+ `/.settings/mounts.damaged-2026-10-11T09-30-00-000Z.json`). Nothing removes these backups; a file there
283
+ means a save replaced settings that could not be read, and the lost entries can be recovered from it
284
+ by hand.
285
+
286
+ ### Constraints
287
+
288
+ - Mount keys are validated (`validateKey`); the key is the first path segment.
289
+ - S3 object stores have no real folders: the prefix is part of every key, and a mount sees only below it.
290
+ - Dependencies: `@statewalker/webrun-files*` for the `FilesApi` contract and the composite, memory,
291
+ browser and S3 implementations; `@aws-sdk/client-s3` for signing and the checks;
292
+ `@statewalker/shared-baseclass` for `onUpdate`.
293
+
294
+ ## Running the S3 suite against a real server
295
+
296
+ The unit suite needs no S3 server or Docker:
297
+
298
+ ```
299
+ pnpm --filter @statewalker/file-mounts.core test
300
+ ```
301
+
302
+ The integration suite starts a RustFS container (`rustfs/rustfs:1.0.0`, a free local port, root keys
303
+ `rustfsadmin`), creates a bucket, and checks: all five checks pass, read-only skips the write, wrong
304
+ keys, missing bucket, files land under the prefix, a copy from one storage into S3. Docker is required;
305
+ without it the tests are skipped. Set `RUSTFS_IMAGE` to use another image.
306
+
307
+ ```
308
+ pnpm --filter @statewalker/file-mounts.core test:integration
309
+ ```
310
+
311
+ The container is removed when the suite ends. A restricted user (denied write) and CORS are not
312
+ produced here: RustFS's root keys can do everything and Node does not enforce CORS. Those outcomes are
313
+ covered by the classifier unit tests.
314
+
315
+ ## License
316
+
317
+ MIT
@@ -0,0 +1,16 @@
1
+ //#region src/changes.d.ts
2
+ /** One change to the files: what an operation or a mount change did. */
3
+ export interface FileChange {
4
+ type: "added" | "updated" | "deleted";
5
+ path: string;
6
+ }
7
+ /** The one stream of changes: panels refresh from it, an indexer will follow it. */
8
+ export interface FileChanges {
9
+ subscribe(listener: (changes: readonly FileChange[]) => void): () => void;
10
+ }
11
+ export interface FileChangesHub extends FileChanges {
12
+ emit(changes: readonly FileChange[]): void;
13
+ }
14
+ export declare function newFileChanges(): FileChangesHub;
15
+ //#endregion
16
+ //# sourceMappingURL=changes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"changes.d.ts","names":[],"sources":["../src/changes.ts"],"mappings":";;iBACiB;EACf;EACA;;;iBAIe;EACf,UAAU,WAAW,kBAAkB;;iBAGxB,uBAAuB;EACtC,KAAK,kBAAkB;;wBAGT,kBAAkB"}
@@ -0,0 +1,18 @@
1
+ //#region src/changes.ts
2
+ function newFileChanges() {
3
+ const listeners = /* @__PURE__ */ new Set();
4
+ return {
5
+ subscribe(listener) {
6
+ listeners.add(listener);
7
+ return () => listeners.delete(listener);
8
+ },
9
+ emit(changes) {
10
+ if (!changes.length) return;
11
+ for (const listener of [...listeners]) listener(changes);
12
+ }
13
+ };
14
+ }
15
+ //#endregion
16
+ export { newFileChanges };
17
+
18
+ //# sourceMappingURL=changes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"changes.js","names":[],"sources":["../src/changes.ts"],"sourcesContent":["/** One change to the files: what an operation or a mount change did. */\nexport interface FileChange {\n type: \"added\" | \"updated\" | \"deleted\";\n path: string;\n}\n\n/** The one stream of changes: panels refresh from it, an indexer will follow it. */\nexport interface FileChanges {\n subscribe(listener: (changes: readonly FileChange[]) => void): () => void;\n}\n\nexport interface FileChangesHub extends FileChanges {\n emit(changes: readonly FileChange[]): void;\n}\n\nexport function newFileChanges(): FileChangesHub {\n const listeners = new Set<(changes: readonly FileChange[]) => void>();\n return {\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n emit(changes) {\n if (!changes.length) return;\n for (const listener of [...listeners]) listener(changes);\n },\n };\n}\n"],"mappings":";AAeA,SAAgB,iBAAiC;CAC/C,MAAM,4BAAY,IAAI,IAA8C;CACpE,OAAO;EACL,UAAU,UAAU;GAClB,UAAU,IAAI,QAAQ;GACtB,aAAa,UAAU,OAAO,QAAQ;EACxC;EACA,KAAK,SAAS;GACZ,IAAI,CAAC,QAAQ,QAAQ;GACrB,KAAK,MAAM,YAAY,CAAC,GAAG,SAAS,GAAG,SAAS,OAAO;EACzD;CACF;AACF"}
@@ -0,0 +1,31 @@
1
+ import { Problem } from "./problem.js";
2
+ import { S3Form } from "./mount-config.js";
3
+ import { CheckId } from "./s3-problems.js";
4
+ //#region src/connection-test.d.ts
5
+ export interface S3Probe {
6
+ /** An unsigned no-cors fetch: true when the host answers at all. */
7
+ hostAnswers(): Promise<boolean>;
8
+ /** Throws an S3-like error when the check fails. */
9
+ check(id: CheckId): Promise<void>;
10
+ }
11
+ export interface ConnectionReport {
12
+ ok: boolean;
13
+ checks: {
14
+ id: CheckId;
15
+ label: string;
16
+ state: "ok" | "failed" | "skipped" | "not-run";
17
+ }[];
18
+ problem?: Problem;
19
+ }
20
+ export declare const CHECK_LABELS: Record<CheckId, string>;
21
+ export declare const CHECK_ORDER: CheckId[];
22
+ export declare function runConnectionTest(probe: S3Probe, form: Pick<S3Form, "endpoint" | "bucket" | "readOnly">, opts: {
23
+ origin: string;
24
+ signal?: AbortSignal;
25
+ timeoutMs?: number;
26
+ }): Promise<ConnectionReport>;
27
+ export type ProbeScenario = "ok" | "bad-keys" | "clock-skew" | "no-bucket" | "denied-list" | "denied-write" | "cors" | "unreachable" | "timeout";
28
+ /** A scripted probe for stories and tests; each check waits `delayMs` so progress is visible. */
29
+ export declare function fakeProbe(scenario: ProbeScenario, delayMs?: number): S3Probe;
30
+ //#endregion
31
+ //# sourceMappingURL=connection-test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-test.d.ts","names":[],"sources":["../src/connection-test.ts"],"mappings":";;;;iBAIiB;;EAEf,eAAe;;EAEf,MAAM,IAAI,UAAU;;iBAGL;EACf;EACA;IAAU,IAAI;IAAS;IAAe;;EACtC,UAAU;;qBAGC,cAAc,OAAO;qBAOrB,aAAa;wBAiDJ,kBACpB,OAAO,SACP,MAAM,KAAK,6CACX;EAAQ;EAAgB,SAAS;EAAa;IAC7C,QAAQ;YA+BC;;wBAqCI,UAAU,UAAU,eAAe,mBAAc"}
@@ -0,0 +1,152 @@
1
+ import { classifyS3Error } from "./s3-problems.js";
2
+ //#region src/connection-test.ts
3
+ const CHECK_LABELS = {
4
+ endpoint: "The address answers",
5
+ keys: "The keys are accepted",
6
+ bucket: "The bucket exists",
7
+ list: "Listing is allowed",
8
+ write: "Writing is allowed"
9
+ };
10
+ const CHECK_ORDER = [
11
+ "endpoint",
12
+ "keys",
13
+ "bucket",
14
+ "list",
15
+ "write"
16
+ ];
17
+ const CANCELLED = Symbol("cancelled");
18
+ function isNetworkError(error) {
19
+ const e = error;
20
+ return e?.name === "NetworkError" || e?.name === "TypeError" && !!e.message?.includes("Failed to fetch");
21
+ }
22
+ /** Runs one check, ending it with a TimeoutError after `timeoutMs` or with CANCELLED on abort. */
23
+ function runCheck(probe, id, timeoutMs, signal) {
24
+ return new Promise((resolve, reject) => {
25
+ const finish = () => {
26
+ clearTimeout(timer);
27
+ signal?.removeEventListener("abort", onAbort);
28
+ };
29
+ const onAbort = () => {
30
+ finish();
31
+ reject(CANCELLED);
32
+ };
33
+ const timer = setTimeout(() => {
34
+ finish();
35
+ const timeout = /* @__PURE__ */ new Error("The check timed out");
36
+ timeout.name = "TimeoutError";
37
+ reject(timeout);
38
+ }, timeoutMs);
39
+ if (signal?.aborted) return onAbort();
40
+ signal?.addEventListener("abort", onAbort);
41
+ probe.check(id).then(() => {
42
+ finish();
43
+ resolve();
44
+ }, (error) => {
45
+ finish();
46
+ reject(error);
47
+ });
48
+ });
49
+ }
50
+ async function runConnectionTest(probe, form, opts) {
51
+ const checks = CHECK_ORDER.map((id) => ({
52
+ id,
53
+ label: CHECK_LABELS[id],
54
+ state: "not-run"
55
+ }));
56
+ for (const entry of checks) {
57
+ if (entry.id === "write" && form.readOnly) {
58
+ entry.state = "skipped";
59
+ continue;
60
+ }
61
+ try {
62
+ await runCheck(probe, entry.id, opts.timeoutMs ?? 15e3, opts.signal);
63
+ entry.state = "ok";
64
+ } catch (error) {
65
+ if (error === CANCELLED) return {
66
+ ok: false,
67
+ checks
68
+ };
69
+ entry.state = "failed";
70
+ const hostAnswers = isNetworkError(error) ? await probe.hostAnswers() : void 0;
71
+ return {
72
+ ok: false,
73
+ checks,
74
+ problem: classifyS3Error(error, {
75
+ check: entry.id,
76
+ endpoint: form.endpoint,
77
+ bucket: form.bucket,
78
+ origin: opts.origin,
79
+ hostAnswers
80
+ })
81
+ };
82
+ }
83
+ }
84
+ return {
85
+ ok: true,
86
+ checks
87
+ };
88
+ }
89
+ const FAILURES = {
90
+ "bad-keys": {
91
+ id: "keys",
92
+ error: () => ({
93
+ name: "InvalidAccessKeyId",
94
+ message: "The access key ID does not exist."
95
+ })
96
+ },
97
+ "clock-skew": {
98
+ id: "keys",
99
+ error: () => ({
100
+ name: "RequestTimeTooSkewed",
101
+ message: "The difference between the request time and the server's time is too large."
102
+ })
103
+ },
104
+ "no-bucket": {
105
+ id: "bucket",
106
+ error: () => ({
107
+ name: "NoSuchBucket",
108
+ message: "The specified bucket does not exist."
109
+ })
110
+ },
111
+ "denied-list": {
112
+ id: "list",
113
+ error: () => ({
114
+ name: "AccessDenied",
115
+ message: "Access Denied"
116
+ })
117
+ },
118
+ "denied-write": {
119
+ id: "write",
120
+ error: () => ({
121
+ name: "AccessDenied",
122
+ message: "Access Denied"
123
+ })
124
+ },
125
+ cors: {
126
+ id: "endpoint",
127
+ error: () => /* @__PURE__ */ new TypeError("Failed to fetch")
128
+ },
129
+ unreachable: {
130
+ id: "endpoint",
131
+ error: () => /* @__PURE__ */ new TypeError("Failed to fetch")
132
+ }
133
+ };
134
+ /** A scripted probe for stories and tests; each check waits `delayMs` so progress is visible. */
135
+ function fakeProbe(scenario, delayMs = 0) {
136
+ const wait = () => delayMs > 0 ? new Promise((r) => setTimeout(r, delayMs)) : Promise.resolve();
137
+ return {
138
+ async hostAnswers() {
139
+ return scenario === "cors";
140
+ },
141
+ async check(id) {
142
+ await wait();
143
+ if (scenario === "timeout" && id === "endpoint") return new Promise(() => {});
144
+ const failure = FAILURES[scenario];
145
+ if (failure && failure.id === id) throw failure.error();
146
+ }
147
+ };
148
+ }
149
+ //#endregion
150
+ export { CHECK_LABELS, CHECK_ORDER, fakeProbe, runConnectionTest };
151
+
152
+ //# sourceMappingURL=connection-test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-test.js","names":[],"sources":["../src/connection-test.ts"],"sourcesContent":["import type { S3Form } from \"./mount-config.js\";\nimport type { Problem } from \"./problem.js\";\nimport { type CheckId, classifyS3Error } from \"./s3-problems.js\";\n\nexport interface S3Probe {\n /** An unsigned no-cors fetch: true when the host answers at all. */\n hostAnswers(): Promise<boolean>;\n /** Throws an S3-like error when the check fails. */\n check(id: CheckId): Promise<void>;\n}\n\nexport interface ConnectionReport {\n ok: boolean;\n checks: { id: CheckId; label: string; state: \"ok\" | \"failed\" | \"skipped\" | \"not-run\" }[];\n problem?: Problem;\n}\n\nexport const CHECK_LABELS: Record<CheckId, string> = {\n endpoint: \"The address answers\",\n keys: \"The keys are accepted\",\n bucket: \"The bucket exists\",\n list: \"Listing is allowed\",\n write: \"Writing is allowed\",\n};\nexport const CHECK_ORDER: CheckId[] = [\"endpoint\", \"keys\", \"bucket\", \"list\", \"write\"];\n\nconst CANCELLED = Symbol(\"cancelled\");\n\nfunction isNetworkError(error: unknown): boolean {\n const e = error as { name?: string; message?: string } | null;\n return (\n e?.name === \"NetworkError\" ||\n (e?.name === \"TypeError\" && !!e.message?.includes(\"Failed to fetch\"))\n );\n}\n\n/** Runs one check, ending it with a TimeoutError after `timeoutMs` or with CANCELLED on abort. */\nfunction runCheck(\n probe: S3Probe,\n id: CheckId,\n timeoutMs: number,\n signal?: AbortSignal,\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const finish = () => {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", onAbort);\n };\n const onAbort = () => {\n finish();\n reject(CANCELLED);\n };\n const timer = setTimeout(() => {\n finish();\n const timeout = new Error(\"The check timed out\");\n timeout.name = \"TimeoutError\";\n reject(timeout);\n }, timeoutMs);\n if (signal?.aborted) return onAbort();\n signal?.addEventListener(\"abort\", onAbort);\n probe.check(id).then(\n () => {\n finish();\n resolve();\n },\n (error) => {\n finish();\n reject(error);\n },\n );\n });\n}\n\nexport async function runConnectionTest(\n probe: S3Probe,\n form: Pick<S3Form, \"endpoint\" | \"bucket\" | \"readOnly\">,\n opts: { origin: string; signal?: AbortSignal; timeoutMs?: number },\n): Promise<ConnectionReport> {\n const checks: ConnectionReport[\"checks\"] = CHECK_ORDER.map((id) => ({\n id,\n label: CHECK_LABELS[id],\n state: \"not-run\",\n }));\n for (const entry of checks) {\n if (entry.id === \"write\" && form.readOnly) {\n entry.state = \"skipped\";\n continue;\n }\n try {\n await runCheck(probe, entry.id, opts.timeoutMs ?? 15000, opts.signal);\n entry.state = \"ok\";\n } catch (error) {\n if (error === CANCELLED) return { ok: false, checks };\n entry.state = \"failed\";\n const hostAnswers = isNetworkError(error) ? await probe.hostAnswers() : undefined;\n const problem = classifyS3Error(error, {\n check: entry.id,\n endpoint: form.endpoint,\n bucket: form.bucket,\n origin: opts.origin,\n hostAnswers,\n });\n return { ok: false, checks, problem };\n }\n }\n return { ok: true, checks };\n}\n\nexport type ProbeScenario =\n | \"ok\"\n | \"bad-keys\"\n | \"clock-skew\"\n | \"no-bucket\"\n | \"denied-list\"\n | \"denied-write\"\n | \"cors\"\n | \"unreachable\"\n | \"timeout\";\n\nconst FAILURES: Partial<Record<ProbeScenario, { id: CheckId; error: () => unknown }>> = {\n \"bad-keys\": {\n id: \"keys\",\n error: () => ({ name: \"InvalidAccessKeyId\", message: \"The access key ID does not exist.\" }),\n },\n \"clock-skew\": {\n id: \"keys\",\n error: () => ({\n name: \"RequestTimeTooSkewed\",\n message: \"The difference between the request time and the server's time is too large.\",\n }),\n },\n \"no-bucket\": {\n id: \"bucket\",\n error: () => ({ name: \"NoSuchBucket\", message: \"The specified bucket does not exist.\" }),\n },\n \"denied-list\": { id: \"list\", error: () => ({ name: \"AccessDenied\", message: \"Access Denied\" }) },\n \"denied-write\": {\n id: \"write\",\n error: () => ({ name: \"AccessDenied\", message: \"Access Denied\" }),\n },\n cors: { id: \"endpoint\", error: () => new TypeError(\"Failed to fetch\") },\n unreachable: { id: \"endpoint\", error: () => new TypeError(\"Failed to fetch\") },\n};\n\n/** A scripted probe for stories and tests; each check waits `delayMs` so progress is visible. */\nexport function fakeProbe(scenario: ProbeScenario, delayMs = 0): S3Probe {\n const wait = () =>\n delayMs > 0 ? new Promise<void>((r) => setTimeout(r, delayMs)) : Promise.resolve();\n return {\n async hostAnswers() {\n return scenario === \"cors\";\n },\n async check(id) {\n await wait();\n if (scenario === \"timeout\" && id === \"endpoint\") return new Promise<void>(() => {});\n const failure = FAILURES[scenario];\n if (failure && failure.id === id) throw failure.error();\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { FileChange, FileChanges, FileChangesHub, newFileChanges } from "./changes.js";
2
+ import { MountUnavailable, Problem, ProblemCause, problemOf } from "./problem.js";
3
+ import { MountConfig, MountStatus, MountTypeId, S3Form, S3Preset, S3PresetId, S3_PRESETS, applyPreset, keyFromName, normalizeEndpoint, normalizePrefix, validateKey, validateS3Form } from "./mount-config.js";
4
+ import { CheckId, S3ErrorLike, classifyS3Error, corsRule } from "./s3-problems.js";
5
+ import { CHECK_LABELS, CHECK_ORDER, ConnectionReport, ProbeScenario, S3Probe, fakeProbe, runConnectionTest } from "./connection-test.js";
6
+ import { IdbStore, KeyValueStore, MemoryStore } from "./key-value-store.js";
7
+ import { MountContext, MountField, MountType, NeedsUserGesture } from "./mount-type.js";
8
+ import { localFolderMountType } from "./local-folder-mount-type.js";
9
+ import { ProfileData, SecretStore, SecretVault, SecretsLocked, VaultState } from "./secret-vault.js";
10
+ import { canBeMain, firstProfileData } from "./main-storage.js";
11
+ import { memoryMountType } from "./memory-mount-type.js";
12
+ import { MOUNTS_FILE, loadMountConfigs, saveMountConfigs } from "./mount-settings-file.js";
13
+ import { FilesMountTable, MountTable } from "./mount-table.js";
14
+ import { opfsMountType } from "./opfs-mount-type.js";
15
+ import { MemoryAuthenticator, PASSKEY_MESSAGES, PasskeyAuthenticator, PasskeyProblem, memoryAuthenticator, webAuthnPasskeys } from "./passkeys.js";
16
+ import { ProfileInfo, Profiles, validateProfileName } from "./profiles.js";
17
+ import { PROJECT_SYSTEM_DIR, ProjectArchive, archiveProfile, importProfile, mergeImported } from "./project-archive.js";
18
+ import { S3Keys, S3Settings, newS3Probe, s3MountType, s3Settings, testConnection } from "./s3.js";
19
+ import { unavailableFiles } from "./unavailable-files.js";
20
+ export { CHECK_LABELS, CHECK_ORDER, CheckId, ConnectionReport, FileChange, FileChanges, FileChangesHub, FilesMountTable, IdbStore, KeyValueStore, MOUNTS_FILE, MemoryAuthenticator, MemoryStore, MountConfig, MountContext, MountField, MountStatus, MountTable, MountType, MountTypeId, MountUnavailable, NeedsUserGesture, PASSKEY_MESSAGES, PROJECT_SYSTEM_DIR, PasskeyAuthenticator, PasskeyProblem, ProbeScenario, Problem, ProblemCause, ProfileData, ProfileInfo, Profiles, ProjectArchive, S3ErrorLike, S3Form, S3Keys, S3Preset, S3PresetId, S3Probe, S3Settings, S3_PRESETS, SecretStore, SecretVault, SecretsLocked, VaultState, applyPreset, archiveProfile, canBeMain, classifyS3Error, corsRule, fakeProbe, firstProfileData, importProfile, keyFromName, loadMountConfigs, localFolderMountType, memoryAuthenticator, memoryMountType, mergeImported, newFileChanges, newS3Probe, normalizeEndpoint, normalizePrefix, opfsMountType, problemOf, runConnectionTest, s3MountType, s3Settings, saveMountConfigs, testConnection, unavailableFiles, validateKey, validateProfileName, validateS3Form, webAuthnPasskeys };
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ import { newFileChanges } from "./changes.js";
2
+ import { classifyS3Error, corsRule } from "./s3-problems.js";
3
+ import { CHECK_LABELS, CHECK_ORDER, fakeProbe, runConnectionTest } from "./connection-test.js";
4
+ import { IdbStore, MemoryStore } from "./key-value-store.js";
5
+ import { NeedsUserGesture } from "./mount-type.js";
6
+ import { MountUnavailable, problemOf } from "./problem.js";
7
+ import { localFolderMountType } from "./local-folder-mount-type.js";
8
+ import { canBeMain, firstProfileData } from "./main-storage.js";
9
+ import { memoryMountType } from "./memory-mount-type.js";
10
+ import { S3_PRESETS, applyPreset, keyFromName, normalizeEndpoint, normalizePrefix, validateKey, validateS3Form } from "./mount-config.js";
11
+ import { MOUNTS_FILE, loadMountConfigs, saveMountConfigs } from "./mount-settings-file.js";
12
+ import { SecretVault, SecretsLocked } from "./secret-vault.js";
13
+ import { unavailableFiles } from "./unavailable-files.js";
14
+ import { FilesMountTable } from "./mount-table.js";
15
+ import { opfsMountType } from "./opfs-mount-type.js";
16
+ import { PASSKEY_MESSAGES, PasskeyProblem, memoryAuthenticator, webAuthnPasskeys } from "./passkeys.js";
17
+ import { Profiles, validateProfileName } from "./profiles.js";
18
+ import { PROJECT_SYSTEM_DIR, archiveProfile, importProfile, mergeImported } from "./project-archive.js";
19
+ import { newS3Probe, s3MountType, s3Settings, testConnection } from "./s3.js";
20
+ export { CHECK_LABELS, CHECK_ORDER, FilesMountTable, IdbStore, MOUNTS_FILE, MemoryStore, MountUnavailable, NeedsUserGesture, PASSKEY_MESSAGES, PROJECT_SYSTEM_DIR, PasskeyProblem, Profiles, S3_PRESETS, SecretVault, SecretsLocked, applyPreset, archiveProfile, canBeMain, classifyS3Error, corsRule, fakeProbe, firstProfileData, importProfile, keyFromName, loadMountConfigs, localFolderMountType, memoryAuthenticator, memoryMountType, mergeImported, newFileChanges, newS3Probe, normalizeEndpoint, normalizePrefix, opfsMountType, problemOf, runConnectionTest, s3MountType, s3Settings, saveMountConfigs, testConnection, unavailableFiles, validateKey, validateProfileName, validateS3Form, webAuthnPasskeys };