@3sln/trove 0.0.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 (162) hide show
  1. package/README.md +1227 -0
  2. package/package.json +75 -0
  3. package/packages/core/src/collections/index.js +249 -0
  4. package/packages/core/src/errors.js +186 -0
  5. package/packages/core/src/identity/discovery.js +210 -0
  6. package/packages/core/src/identity/index.js +188 -0
  7. package/packages/core/src/identity/jwt.js +199 -0
  8. package/packages/core/src/index.js +104 -0
  9. package/packages/core/src/indexers/contribution.js +115 -0
  10. package/packages/core/src/indexers/registry.js +162 -0
  11. package/packages/core/src/indexing.js +340 -0
  12. package/packages/core/src/issues.js +150 -0
  13. package/packages/core/src/kv.js +0 -0
  14. package/packages/core/src/links.js +141 -0
  15. package/packages/core/src/metadata/cursor.js +73 -0
  16. package/packages/core/src/metadata/interface.js +244 -0
  17. package/packages/core/src/metadata/memory.js +270 -0
  18. package/packages/core/src/metadata/sqlite.js +412 -0
  19. package/packages/core/src/notifications/index.js +139 -0
  20. package/packages/core/src/notifications/webpush.js +217 -0
  21. package/packages/core/src/plugins/contributions.js +177 -0
  22. package/packages/core/src/plugins/identity.js +98 -0
  23. package/packages/core/src/plugins/index.js +225 -0
  24. package/packages/core/src/plugins/indexers.js +142 -0
  25. package/packages/core/src/plugins/installStore.js +134 -0
  26. package/packages/core/src/plugins/package.js +102 -0
  27. package/packages/core/src/plugins/packageStore.js +61 -0
  28. package/packages/core/src/plugins/runtime.js +101 -0
  29. package/packages/core/src/plugins/sql.js +52 -0
  30. package/packages/core/src/retry.js +74 -0
  31. package/packages/core/src/scan.js +302 -0
  32. package/packages/core/src/search/embeddings.js +128 -0
  33. package/packages/core/src/search/index.js +200 -0
  34. package/packages/core/src/search/keywordStore.js +107 -0
  35. package/packages/core/src/search/sqliteStores.js +455 -0
  36. package/packages/core/src/search/tagMatch.js +59 -0
  37. package/packages/core/src/search/transformer.js +195 -0
  38. package/packages/core/src/search/vectorStore.js +274 -0
  39. package/packages/core/src/search/vectorize.js +249 -0
  40. package/packages/core/src/sidecar/document.js +213 -0
  41. package/packages/core/src/sidecar/index.js +174 -0
  42. package/packages/core/src/sidecar/manager.js +239 -0
  43. package/packages/core/src/sidecar/store.js +46 -0
  44. package/packages/core/src/signedUrls.js +170 -0
  45. package/packages/core/src/sqlite-d1.js +162 -0
  46. package/packages/core/src/sqlite-driver.js +42 -0
  47. package/packages/core/src/sqlite.js +162 -0
  48. package/packages/core/src/storage/filesystem.js +283 -0
  49. package/packages/core/src/storage/interface.js +222 -0
  50. package/packages/core/src/storage/memory.js +113 -0
  51. package/packages/core/src/storage/prefixed.js +75 -0
  52. package/packages/core/src/storage/s3.js +316 -0
  53. package/packages/core/src/storage/s3sigv4.js +185 -0
  54. package/packages/core/src/tasks.js +228 -0
  55. package/packages/core/src/uploads.js +386 -0
  56. package/packages/core/src/util.js +125 -0
  57. package/packages/core/src/vfs.js +666 -0
  58. package/packages/plugin-sdk/src/browser.js +316 -0
  59. package/packages/plugin-sdk/src/index.js +32 -0
  60. package/packages/plugin-sdk/src/protocol.js +59 -0
  61. package/packages/plugin-sdk/src/rpc.js +95 -0
  62. package/packages/server/src/adapters/bun.js +78 -0
  63. package/packages/server/src/adapters/node.js +115 -0
  64. package/packages/server/src/adapters/staticAssets.js +123 -0
  65. package/packages/server/src/adapters/webDist.js +70 -0
  66. package/packages/server/src/adapters/worker-tasks.js +206 -0
  67. package/packages/server/src/adapters/worker.js +159 -0
  68. package/packages/server/src/cachePolicy.js +34 -0
  69. package/packages/server/src/engine/README.md +88 -0
  70. package/packages/server/src/engine/actions/scanCollection.js +114 -0
  71. package/packages/server/src/engine/index.js +95 -0
  72. package/packages/server/src/engine/lazy.js +25 -0
  73. package/packages/server/src/engine/providers/access.js +363 -0
  74. package/packages/server/src/engine/providers/core.js +405 -0
  75. package/packages/server/src/engine/providers/scan.js +67 -0
  76. package/packages/server/src/index.js +698 -0
  77. package/packages/server/src/manifest.js +98 -0
  78. package/packages/server/src/mcp/auth.js +40 -0
  79. package/packages/server/src/mcp/index.js +213 -0
  80. package/packages/server/src/mcp/protocol.js +181 -0
  81. package/packages/server/src/mcp/tools.js +351 -0
  82. package/packages/server/src/router.js +229 -0
  83. package/packages/server/src/routes.js +1066 -0
  84. package/packages/server/src/scope.js +43 -0
  85. package/packages/web/dist/assets/chunk-4xqbzebh.js +5 -0
  86. package/packages/web/dist/assets/chunk-4xqbzebh.js.map +9 -0
  87. package/packages/web/dist/assets/chunk-h05bxfbs.js +5 -0
  88. package/packages/web/dist/assets/chunk-h05bxfbs.js.map +10 -0
  89. package/packages/web/dist/assets/main-4cxs7prw.js +356 -0
  90. package/packages/web/dist/assets/main-4cxs7prw.js.map +103 -0
  91. package/packages/web/dist/assets/styles-kcx1x337.css +1 -0
  92. package/packages/web/dist/icon.svg +11 -0
  93. package/packages/web/dist/index.html +16 -0
  94. package/packages/web/dist/sql-wasm.wasm +0 -0
  95. package/packages/web/dist/sw.js +186 -0
  96. package/packages/web/src/bl/actions.js +410 -0
  97. package/packages/web/src/bl/activity.js +306 -0
  98. package/packages/web/src/bl/commands.js +274 -0
  99. package/packages/web/src/bl/fileType.js +49 -0
  100. package/packages/web/src/bl/index.js +70 -0
  101. package/packages/web/src/bl/links.js +54 -0
  102. package/packages/web/src/bl/offline.js +268 -0
  103. package/packages/web/src/bl/openers.js +71 -0
  104. package/packages/web/src/bl/pluginInstall.js +59 -0
  105. package/packages/web/src/bl/services.js +143 -0
  106. package/packages/web/src/bl/social.js +234 -0
  107. package/packages/web/src/bl/tagQuery.js +44 -0
  108. package/packages/web/src/main.js +10 -0
  109. package/packages/web/src/platform/api.js +529 -0
  110. package/packages/web/src/platform/commands.js +89 -0
  111. package/packages/web/src/platform/context.js +77 -0
  112. package/packages/web/src/platform/contributions.js +156 -0
  113. package/packages/web/src/platform/index.js +150 -0
  114. package/packages/web/src/platform/keybindings.js +199 -0
  115. package/packages/web/src/platform/mediaUrls.js +137 -0
  116. package/packages/web/src/platform/navigation.js +131 -0
  117. package/packages/web/src/platform/notifications.js +50 -0
  118. package/packages/web/src/platform/overlay.js +81 -0
  119. package/packages/web/src/platform/pluginClientDb.js +132 -0
  120. package/packages/web/src/platform/pluginDock.js +141 -0
  121. package/packages/web/src/platform/pluginFrames.js +194 -0
  122. package/packages/web/src/platform/pluginHost.js +648 -0
  123. package/packages/web/src/platform/pluginMedia.js +62 -0
  124. package/packages/web/src/platform/pluginModules.js +90 -0
  125. package/packages/web/src/platform/pluginNet.js +71 -0
  126. package/packages/web/src/platform/pluginPackage.js +247 -0
  127. package/packages/web/src/platform/pluginRpc.js +377 -0
  128. package/packages/web/src/platform/pluginSigning.js +168 -0
  129. package/packages/web/src/platform/pluginStore.js +67 -0
  130. package/packages/web/src/platform/settings.js +101 -0
  131. package/packages/web/src/platform/spatialNav.js +286 -0
  132. package/packages/web/src/platform/viewport.js +123 -0
  133. package/packages/web/src/platform/voice.js +133 -0
  134. package/packages/web/src/platform/voiceSearch.js +155 -0
  135. package/packages/web/src/platform/whenclause.js +162 -0
  136. package/packages/web/src/platform/workbench.js +156 -0
  137. package/packages/web/src/runtime.js +73 -0
  138. package/packages/web/src/styles.css +1382 -0
  139. package/packages/web/src/ui/components/activityBar.js +35 -0
  140. package/packages/web/src/ui/components/activityPanel.js +132 -0
  141. package/packages/web/src/ui/components/commandPalette.js +154 -0
  142. package/packages/web/src/ui/components/editorArea.js +75 -0
  143. package/packages/web/src/ui/components/launcher.js +392 -0
  144. package/packages/web/src/ui/components/openers/index.js +212 -0
  145. package/packages/web/src/ui/components/openers/markdown.js +222 -0
  146. package/packages/web/src/ui/components/overlays.js +255 -0
  147. package/packages/web/src/ui/components/phoneChrome.js +188 -0
  148. package/packages/web/src/ui/components/pluginReview.js +151 -0
  149. package/packages/web/src/ui/components/pluginsView.js +120 -0
  150. package/packages/web/src/ui/components/settingsView.js +258 -0
  151. package/packages/web/src/ui/components/social.js +290 -0
  152. package/packages/web/src/ui/components/statusBar.js +198 -0
  153. package/packages/web/src/ui/components/views/grid.js +115 -0
  154. package/packages/web/src/ui/components/views/index.js +155 -0
  155. package/packages/web/src/ui/components/views/list.js +50 -0
  156. package/packages/web/src/ui/components/views/parts.js +58 -0
  157. package/packages/web/src/ui/compositions/workbench.js +125 -0
  158. package/packages/web/src/ui/format.js +33 -0
  159. package/packages/web/src/ui/icon.js +81 -0
  160. package/packages/web/src/ui/media.js +114 -0
  161. package/packages/web/src/ui/sanitize.js +86 -0
  162. package/packages/web/src/workbench.js +205 -0
@@ -0,0 +1,363 @@
1
+ // Authorization you hold, rather than authorization you remembered to check.
2
+ //
3
+ // The shape this replaces was everywhere: stat the node, look up its collection,
4
+ // assert a capability, then operate — through an unrestricted `vfs`, using a raw
5
+ // id. Twenty-six sites, each of which had to remember all four steps in order.
6
+ // The check and the use were separate, so nothing carried the grant forward: a
7
+ // caller who asserted `read` still held a `vfs` and an id, and `vfs.remove(id)`
8
+ // was one line away.
9
+ //
10
+ // Here the grant IS the object you operate through. `obtain` resolves the node,
11
+ // asserts the capability, and hands back a handle whose METHODS ARE THE ONES
12
+ // THAT CAPABILITY PERMITS — a read handle has no `remove`, because there is no
13
+ // `remove` to call, not because calling it is checked. The action never sees a
14
+ // raw id it could take somewhere else; `id` is on the handle as data, for the
15
+ // response, and there is nowhere else to take it.
16
+ //
17
+ // And because this is a provider, denial happens during `container.lease()` —
18
+ // before `execute` runs at all. An action cannot proceed unauthorized because it
19
+ // cannot obtain its dependencies. That works identically for actions and
20
+ // queries, which an interceptor could not: ngin runs interceptors on the
21
+ // dispatcher only, so a query would have escaped one entirely.
22
+
23
+ import { Provider } from '@3sln/ngin';
24
+ import { TroveError } from '@3sln/trove/core';
25
+
26
+ /**
27
+ * The one place that decides whether this deployment enforces ACLs at all.
28
+ *
29
+ * From configuration, never from whether a service is present — those agree when
30
+ * everything is wired correctly and diverge exactly when it is not, and the
31
+ * second stops enforcing at the worst possible moment.
32
+ */
33
+ const enforcing = (config) => config?.collections !== false;
34
+
35
+ /** Conversations are optional; a drive without them says so rather than crashing. */
36
+ function requireSidecar(sidecar) {
37
+ if (!sidecar) throw TroveError.unsupported('Conversations are not enabled on this server');
38
+ return sidecar;
39
+ }
40
+
41
+
42
+ /**
43
+ * What the principal actually holds — never a second opinion about it.
44
+ *
45
+ * An earlier draft of this file decided implication here: `write` implies
46
+ * `read`, `delete` implies `read`. CollectionService does not agree — only
47
+ * `admin` implies anything (see `expand`), and a grant of `['write']` alone
48
+ * carries no read. So a handle obtained with `capability: 'write'` was handing
49
+ * out `read`, `download` and `view` to a principal the ACL had never given them
50
+ * to. Two models of the same rule, and the more permissive one winning.
51
+ *
52
+ * There is one model now, and it lives in CollectionService. This asks — and
53
+ * then intersects with what the caller ASKED for, so a handle stays as narrow as
54
+ * the request that made it. An admin who obtains a `read` handle gets a read
55
+ * handle; holding a capability and wielding it are different things, and an
56
+ * operation that only needs to read should not be one typo from deleting.
57
+ */
58
+ const ALL = new Set(['read', 'write', 'delete', 'admin']);
59
+
60
+ /** One list of what a capability can be, so a typo is refused rather than read as none. */
61
+ function assertCapability(capability) {
62
+ if (!ALL.has(capability)) throw TroveError.invalid(`Unknown capability "${capability}"`);
63
+ }
64
+
65
+ /** What naming `capability` asks for, under CollectionService's own implication. */
66
+ function requested(capability) {
67
+ return capability === 'admin' ? new Set(ALL) : new Set([capability]);
68
+ }
69
+
70
+ const intersect = (held, asked) => new Set([...asked].filter((c) => held.has(c)));
71
+
72
+ /**
73
+ * One node, and the operations the caller may perform on it.
74
+ *
75
+ * @param {object} vfs
76
+ * @param {object} node already resolved by `stat`
77
+ * @param {string|symbol} granted the capability that was asserted
78
+ */
79
+ function nodeHandle(vfs, sidecar, node, held) {
80
+ const permits = (capability) => held.has(capability);
81
+ const granted = held === ALL ? 'system' : [...held].sort().join(',');
82
+ // Data, not authority. Present so a response can name what it acted on.
83
+ const handle = {
84
+ id: node.id,
85
+ name: node.name,
86
+ collectionId: node.collectionId,
87
+ contentType: node.contentType,
88
+ size: node.size,
89
+ node,
90
+ granted,
91
+ };
92
+
93
+ // Conversations and tags live in the sidecar, not the vfs — but they are still
94
+ // operations ON THIS NODE, so the same grant has to reach them. Fronting only
95
+ // the vfs would have left eight routes asserting a capability and then working
96
+ // through an unrestricted service, which is the shape this exists to remove.
97
+ //
98
+ // Reading a file's conversation is reading the file: the comments on it are as
99
+ // much its content as its bytes are.
100
+ if (permits('read')) {
101
+ handle.read = (opts) => vfs.readStream(node.id, opts);
102
+ handle.download = (opts) => vfs.getDownload(node.id, opts);
103
+ // Minting a URL that carries its own grant is an exercise of `read` — you are
104
+ // delegating the read you hold, to something that cannot present credentials. So it
105
+ // hangs off the read handle like everything else, and a caller without `read`
106
+ // cannot mint one because there is no method to call.
107
+ handle.mintUrl = (opts) => vfs.mintUrl(node.id, opts);
108
+ handle.backlinks = (opts) => vfs.backlinks(node.id, opts);
109
+ handle.view = () => requireSidecar(sidecar).view(node.id);
110
+ handle.subscribe = (principal, muted) => requireSidecar(sidecar).subscribe(node.id, principal, muted);
111
+ handle.unsubscribe = (principal) => requireSidecar(sidecar).unsubscribe(node.id, principal);
112
+ }
113
+
114
+ if (permits('write')) {
115
+ handle.rename = (newName) => vfs.rename(node.id, newName);
116
+ handle.setTag = (name, value, principal) => vfs.setTag(node.id, name, value, principal);
117
+ handle.removeTag = (name, principal) => vfs.removeTag(node.id, name, principal);
118
+ handle.contribute = (contributorId, contribution) =>
119
+ vfs.indexContributions(node.id, contributorId, contribution);
120
+ handle.comment = (comment, principal) => requireSidecar(sidecar).addComment(node.id, comment, principal);
121
+ handle.editComment = (cid, body, principal) =>
122
+ requireSidecar(sidecar).editComment(node.id, cid, body, principal);
123
+ handle.deleteComment = (cid, principal) =>
124
+ requireSidecar(sidecar).deleteComment(node.id, cid, principal);
125
+ handle.react = (cid, emoji, on, principal) =>
126
+ requireSidecar(sidecar).react(node.id, cid, emoji, on, principal);
127
+ }
128
+
129
+ if (permits('delete')) {
130
+ handle.remove = (opts) => vfs.remove(node.id, opts);
131
+ handle.restore = () => vfs.restore(node.id);
132
+ }
133
+
134
+ return handle;
135
+ }
136
+
137
+ /** One collection, and the operations the caller may perform in it. */
138
+ function collectionHandle(vfs, collectionId, held) {
139
+ const permits = (capability) => held.has(capability);
140
+ const handle = {
141
+ id: collectionId,
142
+ granted: held === ALL ? 'system' : [...held].sort().join(','),
143
+ };
144
+
145
+ if (permits('read')) {
146
+ handle.list = (opts) => vfs.list(collectionId, opts);
147
+ handle.usage = () => vfs.storageUsage(collectionId);
148
+ handle.storage = () => vfs.storageFor(collectionId);
149
+ }
150
+
151
+ if (permits('write')) {
152
+ handle.createUpload = (req) => vfs.createUpload({ ...req, collectionId });
153
+ handle.writeFile = (name, body, opts) => vfs.writeFile(name, body, { ...opts, collectionId });
154
+ }
155
+
156
+ if (permits('delete')) {
157
+ // The trash follows `delete`, not `read`. Seeing what you deleted and undoing it
158
+ // are not lesser rights than deleting — and its contents are items that are no
159
+ // longer part of the drive, which a reader has no business enumerating.
160
+ handle.listTrash = (opts) => vfs.listTrash(collectionId, opts);
161
+ // Scoped by listing THIS collection's trash and destroying those rows.
162
+ //
163
+ // `vfs.purgeTrash` is the retention sweeper: it takes `{ before, limit }` and no
164
+ // collection at all, because it runs for the whole drive on a timer. Handing it a
165
+ // `collectionId` it does not read looked collection-scoped and was not — a `delete`
166
+ // grant on one collection would have emptied every other collection's trash.
167
+ handle.purgeTrash = async ({ limit = 1000 } = {}) => {
168
+ const trash = await vfs.listTrash(collectionId, { limit });
169
+ let purged = 0;
170
+ for (const node of trash) {
171
+ await vfs.remove(node.id, { permanent: true }).then(() => { purged++; }).catch(() => {});
172
+ }
173
+ return { purged };
174
+ };
175
+ }
176
+
177
+ return handle;
178
+ }
179
+
180
+ /**
181
+ * `deps = { node: { principal, id, capability } }`
182
+ *
183
+ * Resolves the node, asserts the capability on ITS collection, and yields a
184
+ * handle shaped by what was granted. Throws — so the lease fails and the action
185
+ * never runs — when the node is missing or the capability is not held.
186
+ */
187
+ export class NodeAccessProvider extends Provider {
188
+ static deps = ['vfs', 'sidecar', 'collections', 'config', 'signedUrls'];
189
+
190
+ constructor({ vfs, sidecar, collections, config, signedUrls }) {
191
+ super();
192
+ this.vfs = vfs;
193
+ this.sidecar = sidecar;
194
+ this.collections = collections;
195
+ this.config = config;
196
+ this.signedUrls = signedUrls;
197
+ }
198
+
199
+ // `id` is anything `stat` resolves: a node id, a `trove:` URI, or a name — and a
200
+ // name is only unique within a collection, hence the hint. The capability is still
201
+ // asserted on the collection the node TURNS OUT to be in, never on the hint, so
202
+ // naming the wrong collection finds nothing rather than authorizing against it.
203
+ // `trashed: true` widens resolution to include the trash, which `stat` deliberately
204
+ // cannot see — restoring and permanently deleting are the only operations on items
205
+ // that are no longer part of the drive. It widens WHAT IS VISIBLE, never what is
206
+ // permitted: the capability is asserted exactly the same way afterwards.
207
+ async obtain({ principal, id, collectionId, trashed = false, capability = 'read', signature = null } = {}) {
208
+ if (!id) throw TroveError.invalid('A node id is required');
209
+ assertCapability(capability);
210
+ const vfs = await this.vfs.obtain();
211
+ const sidecar = await this.sidecar.obtain();
212
+ const node = trashed ? await vfs.statAny(id, collectionId) : await vfs.stat(id, collectionId);
213
+
214
+ // A signature IS the grant.
215
+ //
216
+ // It was minted by someone who held `read` on this node, and it says so in a way
217
+ // this server can check without asking anyone. So a request carrying a valid one is
218
+ // served without a principal — which is the entire point, because the things that
219
+ // need this (an <img src>, a <video src>, cache.add(), an external API an indexer
220
+ // handed a URL to) cannot send credentials at all.
221
+ //
222
+ // It yields `read` and only `read`, on the one node it names. The capability the
223
+ // CALLER asked for is intersected in, so a signed request can never reach a route
224
+ // that wanted `write` — it fails to obtain rather than quietly acting with less.
225
+ if (signature) {
226
+ // Refused, not narrowed. On the ordinary path `collections.assert` throws when the
227
+ // capability is not held, and the intersect below is a second narrowing on top of
228
+ // it — so intersecting alone here would have answered `capability: 'admin'` with a
229
+ // quiet read handle instead of a denial, and a route that asked for admin would
230
+ // have believed it had it.
231
+ if (capability !== 'read') throw TroveError.forbidden('This link only grants reading');
232
+ const signer = await this.signedUrls.obtain();
233
+ const verdict = await signer.check({ ...signature, id: node.id });
234
+ if (!verdict.ok) throw TroveError.forbidden(`This link is no longer valid (${verdict.reason})`);
235
+ return nodeHandle(vfs, sidecar, node, new Set(['read']));
236
+ }
237
+
238
+ const config = await this.config.obtain();
239
+ if (!enforcing(config)) return nodeHandle(vfs, sidecar, node, requested(capability));
240
+
241
+ // `assert` throws when the capability is not held. Nothing here decides from
242
+ // presence: if the service is missing this raises, it does not allow.
243
+ const collections = await this.collections.obtain();
244
+ const collection = await collections.assert(principal, node.collectionId, capability);
245
+ // Shaped by what the principal HOLDS — asked of the one thing that knows —
246
+ // narrowed to what this call asked for.
247
+ return nodeHandle(vfs, sidecar, node,
248
+ intersect(collections.capabilities(principal, collection), requested(capability)));
249
+ }
250
+
251
+ // vfs is a lazy singleton: its release is a no-op and the handle holds nothing
252
+ // else, so there is nothing to give back.
253
+ release() {}
254
+ }
255
+
256
+ /**
257
+ * `deps = { collection: { principal, id, capability } }`
258
+ *
259
+ * The same, for operations named by collection rather than by node — listing,
260
+ * usage, starting an upload.
261
+ */
262
+ export class CollectionAccessProvider extends Provider {
263
+ static deps = ['vfs', 'collections', 'config'];
264
+
265
+ constructor({ vfs, collections, config }) {
266
+ super();
267
+ this.vfs = vfs;
268
+ this.collections = collections;
269
+ this.config = config;
270
+ }
271
+
272
+ async obtain({ principal, id = 'default', capability = 'read' } = {}) {
273
+ assertCapability(capability);
274
+ const vfs = await this.vfs.obtain();
275
+ const config = await this.config.obtain();
276
+ if (!enforcing(config)) return collectionHandle(vfs, id, requested(capability));
277
+
278
+ const collections = await this.collections.obtain();
279
+ const collection = await collections.assert(principal, id, capability);
280
+ return collectionHandle(vfs, id,
281
+ intersect(collections.capabilities(principal, collection), requested(capability)));
282
+ }
283
+
284
+ release() {}
285
+ }
286
+
287
+ /**
288
+ * `deps = { upload: { principal, id } }`
289
+ *
290
+ * An upload is a conversation, not a call: negotiate, sign or push parts, report them,
291
+ * commit. It spans several requests keyed only by an unguessable `uploadId`, which is
292
+ * why every one of them has to re-check — a grant revoked between `POST /api/uploads`
293
+ * and `complete` must stop the upload, and "we checked at the start" would not.
294
+ *
295
+ * The check was `assertUploadCap`: look the session up to learn its collection, assert
296
+ * `write` there, then drive an unrestricted `vfs` with the raw id. Six routes, and the
297
+ * grant was thrown away in every one. Here the session IS the handle.
298
+ */
299
+ export class UploadAccessProvider extends Provider {
300
+ static deps = ['vfs', 'collections', 'config'];
301
+
302
+ constructor({ vfs, collections, config }) {
303
+ super();
304
+ this.vfs = vfs;
305
+ this.collections = collections;
306
+ this.config = config;
307
+ }
308
+
309
+ async obtain({ principal, id } = {}) {
310
+ if (!id) throw TroveError.invalid('An upload id is required');
311
+ const vfs = await this.vfs.obtain();
312
+ // Resolving first is what makes the check possible at all: only the session knows
313
+ // which collection the bytes are destined for.
314
+ const session = await vfs.uploadStatus(id);
315
+ const config = await this.config.obtain();
316
+ if (enforcing(config)) {
317
+ const collections = await this.collections.obtain();
318
+ await collections.assert(principal, session.collectionId, 'write');
319
+ }
320
+ return {
321
+ id,
322
+ collectionId: session.collectionId,
323
+ status: () => vfs.uploadStatus(id),
324
+ signPart: (n) => vfs.signUploadPart(id, n),
325
+ reportPart: (n, etag) => vfs.reportUploadPart(id, n, etag),
326
+ uploadPart: (n, bodyStream, opts) => vfs.uploadPart(id, n, bodyStream, opts),
327
+ complete: (parts) => vfs.completeUpload(id, parts),
328
+ abort: () => vfs.abortUpload(id),
329
+ };
330
+ }
331
+
332
+ release() {}
333
+ }
334
+
335
+ /**
336
+ * The background domain's grant: unattended work, no user behind it.
337
+ *
338
+ * Separate providers rather than a `system: true` option on the ones above, and
339
+ * that distinction is the whole point. An option is a value in a declaration —
340
+ * it can be copied from another action, set from a variable, or arrive from
341
+ * somewhere it should not. A separate provider has to be named in `deps` by an
342
+ * action that means it, which puts "this runs unauthorized" in the one place a
343
+ * reader is already looking to find out what an action touches.
344
+ */
345
+ export class SystemNodeProvider extends Provider {
346
+ static deps = ['vfs', 'sidecar'];
347
+ constructor({ vfs, sidecar }) { super(); this.vfs = vfs; this.sidecar = sidecar; }
348
+ async obtain({ id } = {}) {
349
+ if (!id) throw TroveError.invalid('A node id is required');
350
+ const vfs = await this.vfs.obtain();
351
+ return nodeHandle(vfs, await this.sidecar.obtain(), await vfs.stat(id), ALL);
352
+ }
353
+ release() {}
354
+ }
355
+
356
+ export class SystemCollectionProvider extends Provider {
357
+ static deps = ['vfs'];
358
+ constructor({ vfs }) { super(); this.vfs = vfs; }
359
+ async obtain({ id = 'default' } = {}) {
360
+ return collectionHandle(await this.vfs.obtain(), id, ALL);
361
+ }
362
+ release() {}
363
+ }