@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.
- package/README.md +1227 -0
- package/package.json +75 -0
- package/packages/core/src/collections/index.js +249 -0
- package/packages/core/src/errors.js +186 -0
- package/packages/core/src/identity/discovery.js +210 -0
- package/packages/core/src/identity/index.js +188 -0
- package/packages/core/src/identity/jwt.js +199 -0
- package/packages/core/src/index.js +104 -0
- package/packages/core/src/indexers/contribution.js +115 -0
- package/packages/core/src/indexers/registry.js +162 -0
- package/packages/core/src/indexing.js +340 -0
- package/packages/core/src/issues.js +150 -0
- package/packages/core/src/kv.js +0 -0
- package/packages/core/src/links.js +141 -0
- package/packages/core/src/metadata/cursor.js +73 -0
- package/packages/core/src/metadata/interface.js +244 -0
- package/packages/core/src/metadata/memory.js +270 -0
- package/packages/core/src/metadata/sqlite.js +412 -0
- package/packages/core/src/notifications/index.js +139 -0
- package/packages/core/src/notifications/webpush.js +217 -0
- package/packages/core/src/plugins/contributions.js +177 -0
- package/packages/core/src/plugins/identity.js +98 -0
- package/packages/core/src/plugins/index.js +225 -0
- package/packages/core/src/plugins/indexers.js +142 -0
- package/packages/core/src/plugins/installStore.js +134 -0
- package/packages/core/src/plugins/package.js +102 -0
- package/packages/core/src/plugins/packageStore.js +61 -0
- package/packages/core/src/plugins/runtime.js +101 -0
- package/packages/core/src/plugins/sql.js +52 -0
- package/packages/core/src/retry.js +74 -0
- package/packages/core/src/scan.js +302 -0
- package/packages/core/src/search/embeddings.js +128 -0
- package/packages/core/src/search/index.js +200 -0
- package/packages/core/src/search/keywordStore.js +107 -0
- package/packages/core/src/search/sqliteStores.js +455 -0
- package/packages/core/src/search/tagMatch.js +59 -0
- package/packages/core/src/search/transformer.js +195 -0
- package/packages/core/src/search/vectorStore.js +274 -0
- package/packages/core/src/search/vectorize.js +249 -0
- package/packages/core/src/sidecar/document.js +213 -0
- package/packages/core/src/sidecar/index.js +174 -0
- package/packages/core/src/sidecar/manager.js +239 -0
- package/packages/core/src/sidecar/store.js +46 -0
- package/packages/core/src/signedUrls.js +170 -0
- package/packages/core/src/sqlite-d1.js +162 -0
- package/packages/core/src/sqlite-driver.js +42 -0
- package/packages/core/src/sqlite.js +162 -0
- package/packages/core/src/storage/filesystem.js +283 -0
- package/packages/core/src/storage/interface.js +222 -0
- package/packages/core/src/storage/memory.js +113 -0
- package/packages/core/src/storage/prefixed.js +75 -0
- package/packages/core/src/storage/s3.js +316 -0
- package/packages/core/src/storage/s3sigv4.js +185 -0
- package/packages/core/src/tasks.js +228 -0
- package/packages/core/src/uploads.js +386 -0
- package/packages/core/src/util.js +125 -0
- package/packages/core/src/vfs.js +666 -0
- package/packages/plugin-sdk/src/browser.js +316 -0
- package/packages/plugin-sdk/src/index.js +32 -0
- package/packages/plugin-sdk/src/protocol.js +59 -0
- package/packages/plugin-sdk/src/rpc.js +95 -0
- package/packages/server/src/adapters/bun.js +78 -0
- package/packages/server/src/adapters/node.js +115 -0
- package/packages/server/src/adapters/staticAssets.js +123 -0
- package/packages/server/src/adapters/webDist.js +70 -0
- package/packages/server/src/adapters/worker-tasks.js +206 -0
- package/packages/server/src/adapters/worker.js +159 -0
- package/packages/server/src/cachePolicy.js +34 -0
- package/packages/server/src/engine/README.md +88 -0
- package/packages/server/src/engine/actions/scanCollection.js +114 -0
- package/packages/server/src/engine/index.js +95 -0
- package/packages/server/src/engine/lazy.js +25 -0
- package/packages/server/src/engine/providers/access.js +363 -0
- package/packages/server/src/engine/providers/core.js +405 -0
- package/packages/server/src/engine/providers/scan.js +67 -0
- package/packages/server/src/index.js +698 -0
- package/packages/server/src/manifest.js +98 -0
- package/packages/server/src/mcp/auth.js +40 -0
- package/packages/server/src/mcp/index.js +213 -0
- package/packages/server/src/mcp/protocol.js +181 -0
- package/packages/server/src/mcp/tools.js +351 -0
- package/packages/server/src/router.js +229 -0
- package/packages/server/src/routes.js +1066 -0
- package/packages/server/src/scope.js +43 -0
- package/packages/web/dist/assets/chunk-4xqbzebh.js +5 -0
- package/packages/web/dist/assets/chunk-4xqbzebh.js.map +9 -0
- package/packages/web/dist/assets/chunk-h05bxfbs.js +5 -0
- package/packages/web/dist/assets/chunk-h05bxfbs.js.map +10 -0
- package/packages/web/dist/assets/main-4cxs7prw.js +356 -0
- package/packages/web/dist/assets/main-4cxs7prw.js.map +103 -0
- package/packages/web/dist/assets/styles-kcx1x337.css +1 -0
- package/packages/web/dist/icon.svg +11 -0
- package/packages/web/dist/index.html +16 -0
- package/packages/web/dist/sql-wasm.wasm +0 -0
- package/packages/web/dist/sw.js +186 -0
- package/packages/web/src/bl/actions.js +410 -0
- package/packages/web/src/bl/activity.js +306 -0
- package/packages/web/src/bl/commands.js +274 -0
- package/packages/web/src/bl/fileType.js +49 -0
- package/packages/web/src/bl/index.js +70 -0
- package/packages/web/src/bl/links.js +54 -0
- package/packages/web/src/bl/offline.js +268 -0
- package/packages/web/src/bl/openers.js +71 -0
- package/packages/web/src/bl/pluginInstall.js +59 -0
- package/packages/web/src/bl/services.js +143 -0
- package/packages/web/src/bl/social.js +234 -0
- package/packages/web/src/bl/tagQuery.js +44 -0
- package/packages/web/src/main.js +10 -0
- package/packages/web/src/platform/api.js +529 -0
- package/packages/web/src/platform/commands.js +89 -0
- package/packages/web/src/platform/context.js +77 -0
- package/packages/web/src/platform/contributions.js +156 -0
- package/packages/web/src/platform/index.js +150 -0
- package/packages/web/src/platform/keybindings.js +199 -0
- package/packages/web/src/platform/mediaUrls.js +137 -0
- package/packages/web/src/platform/navigation.js +131 -0
- package/packages/web/src/platform/notifications.js +50 -0
- package/packages/web/src/platform/overlay.js +81 -0
- package/packages/web/src/platform/pluginClientDb.js +132 -0
- package/packages/web/src/platform/pluginDock.js +141 -0
- package/packages/web/src/platform/pluginFrames.js +194 -0
- package/packages/web/src/platform/pluginHost.js +648 -0
- package/packages/web/src/platform/pluginMedia.js +62 -0
- package/packages/web/src/platform/pluginModules.js +90 -0
- package/packages/web/src/platform/pluginNet.js +71 -0
- package/packages/web/src/platform/pluginPackage.js +247 -0
- package/packages/web/src/platform/pluginRpc.js +377 -0
- package/packages/web/src/platform/pluginSigning.js +168 -0
- package/packages/web/src/platform/pluginStore.js +67 -0
- package/packages/web/src/platform/settings.js +101 -0
- package/packages/web/src/platform/spatialNav.js +286 -0
- package/packages/web/src/platform/viewport.js +123 -0
- package/packages/web/src/platform/voice.js +133 -0
- package/packages/web/src/platform/voiceSearch.js +155 -0
- package/packages/web/src/platform/whenclause.js +162 -0
- package/packages/web/src/platform/workbench.js +156 -0
- package/packages/web/src/runtime.js +73 -0
- package/packages/web/src/styles.css +1382 -0
- package/packages/web/src/ui/components/activityBar.js +35 -0
- package/packages/web/src/ui/components/activityPanel.js +132 -0
- package/packages/web/src/ui/components/commandPalette.js +154 -0
- package/packages/web/src/ui/components/editorArea.js +75 -0
- package/packages/web/src/ui/components/launcher.js +392 -0
- package/packages/web/src/ui/components/openers/index.js +212 -0
- package/packages/web/src/ui/components/openers/markdown.js +222 -0
- package/packages/web/src/ui/components/overlays.js +255 -0
- package/packages/web/src/ui/components/phoneChrome.js +188 -0
- package/packages/web/src/ui/components/pluginReview.js +151 -0
- package/packages/web/src/ui/components/pluginsView.js +120 -0
- package/packages/web/src/ui/components/settingsView.js +258 -0
- package/packages/web/src/ui/components/social.js +290 -0
- package/packages/web/src/ui/components/statusBar.js +198 -0
- package/packages/web/src/ui/components/views/grid.js +115 -0
- package/packages/web/src/ui/components/views/index.js +155 -0
- package/packages/web/src/ui/components/views/list.js +50 -0
- package/packages/web/src/ui/components/views/parts.js +58 -0
- package/packages/web/src/ui/compositions/workbench.js +125 -0
- package/packages/web/src/ui/format.js +33 -0
- package/packages/web/src/ui/icon.js +81 -0
- package/packages/web/src/ui/media.js +114 -0
- package/packages/web/src/ui/sanitize.js +86 -0
- 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
|
+
}
|