@3sln/trove 0.0.17 → 0.0.18
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/package.json +1 -1
- package/packages/core/src/plugins/runtime.js +61 -5
- package/packages/core/src/sidecar/document.js +60 -2
- package/packages/core/src/sidecar/index.js +28 -1
- package/packages/plugin-sdk/src/browser.js +257 -26
- package/packages/plugin-sdk/src/protocol.js +9 -0
- package/packages/server/src/routes.js +74 -0
- package/packages/web/dist/assets/main-b9jd0fyt.js +742 -0
- package/packages/web/dist/assets/{main-wpfmmbfd.js.map → main-b9jd0fyt.js.map} +12 -11
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +33 -3
- package/packages/web/src/bl/fileType.js +8 -0
- package/packages/web/src/bl/launcher.js +15 -0
- package/packages/web/src/platform/index.js +8 -0
- package/packages/web/src/platform/itemData.js +161 -0
- package/packages/web/src/platform/navigation.js +16 -2
- package/packages/web/src/platform/pluginRpc.js +55 -0
- package/packages/web/src/ui/components/launcher.js +1 -1
- package/packages/web/src/ui/components/views/index.js +14 -1
- package/packages/web/dist/assets/main-wpfmmbfd.js +0 -511
|
@@ -373,6 +373,29 @@ export function createRouter() {
|
|
|
373
373
|
return { node: handle.node };
|
|
374
374
|
});
|
|
375
375
|
|
|
376
|
+
// The same question with no collection in the path, for a caller that has an id.
|
|
377
|
+
//
|
|
378
|
+
// `api.stat(ref)` builds this URL whenever it is not given a collection, which is the
|
|
379
|
+
// normal case — an id and a `trove:` URI each name themselves, so there is nothing to
|
|
380
|
+
// scope. The route did not exist, so every such call answered "No such route": that is
|
|
381
|
+
// what broke `ctx.files.blob()` for plugins, and with it every viewer that reads its own
|
|
382
|
+
// file. The audiobook player showed it twice over — no cover art, and no sample tables
|
|
383
|
+
// to stream from — and on the drive it simply said "No such route" and stopped.
|
|
384
|
+
//
|
|
385
|
+
// A NAME is refused here rather than guessed at. A name is only unique inside a
|
|
386
|
+
// collection, so resolving one without saying which collection is a question with more
|
|
387
|
+
// than one right answer.
|
|
388
|
+
r.get('/api/items/resolve', [], async (ctx) => {
|
|
389
|
+
const ref = ctx.query.id || ctx.query.uri;
|
|
390
|
+
if (!ref) {
|
|
391
|
+
throw TroveError.invalid(ctx.query.name
|
|
392
|
+
? 'a name is only unique within a collection — resolve it at /api/collections/:collection/items/resolve'
|
|
393
|
+
: 'id or uri is required');
|
|
394
|
+
}
|
|
395
|
+
const handle = await ctx.access.node(ref, 'read');
|
|
396
|
+
return { node: handle.node };
|
|
397
|
+
});
|
|
398
|
+
|
|
376
399
|
// What links to this item — the inverse of the links its own content declares, and
|
|
377
400
|
// what replaces "which folder is it in?".
|
|
378
401
|
r.get('/api/items/backlinks', ['collections'], async (ctx) => {
|
|
@@ -854,6 +877,24 @@ export function createRouter() {
|
|
|
854
877
|
admin: ctx.collections.isAdmin(ctx.principal),
|
|
855
878
|
}));
|
|
856
879
|
|
|
880
|
+
/**
|
|
881
|
+
* Which plugin scope this caller may write, from `?scope=`.
|
|
882
|
+
*
|
|
883
|
+
* A contribution URI names a plugin, and the caller must have it installed — otherwise
|
|
884
|
+
* "scoped per plugin" is a naming convention rather than a boundary, and one plugin
|
|
885
|
+
* could read or overwrite another's state on every item in the drive.
|
|
886
|
+
*/
|
|
887
|
+
async function requireScope(ctx) {
|
|
888
|
+
const scope = String(ctx.query.scope || '').trim();
|
|
889
|
+
if (!scope) throw TroveError.invalid('scope is required');
|
|
890
|
+
const pluginId = parseContribUri(scope)?.pluginId ?? (scope.startsWith('trove+plugin:') ? scope.slice('trove+plugin:'.length) : null);
|
|
891
|
+
if (!pluginId) throw TroveError.invalid('scope must be a trove+plugin: URI');
|
|
892
|
+
requirePlugins(ctx.plugins);
|
|
893
|
+
const installed = await ctx.plugins.get(ctx.principal, pluginId);
|
|
894
|
+
if (!installed) throw TroveError.forbidden(`"${pluginId}" is not installed for this account`);
|
|
895
|
+
return `trove+plugin:${pluginId}`;
|
|
896
|
+
}
|
|
897
|
+
|
|
857
898
|
// --- conversations, tags, sidecar (per file) -------------------------------
|
|
858
899
|
// The :id is a file node id; the sidecar is that file's CRDT document.
|
|
859
900
|
|
|
@@ -863,6 +904,39 @@ export function createRouter() {
|
|
|
863
904
|
return (await ctx.access.node(ctx.params.id, 'read')).view();
|
|
864
905
|
});
|
|
865
906
|
|
|
907
|
+
// A PLUGIN's own key/value data for this item — a listening position, a last page.
|
|
908
|
+
//
|
|
909
|
+
// The scope is `?scope=` and the route does NOT trust it on its own: `requireScope`
|
|
910
|
+
// checks the caller owns that plugin id, the same gate `/api/index/:indexerId` applies
|
|
911
|
+
// to contributions. Without it any caller could write into any plugin's namespace, and
|
|
912
|
+
// "scoped" would be a naming convention rather than a boundary.
|
|
913
|
+
//
|
|
914
|
+
// `read` to read and `write` to write, on the ITEM — this is data about someone's use of
|
|
915
|
+
// a file, so it follows the file's permissions.
|
|
916
|
+
r.get('/api/items/:id/data', ['sidecar'], async (ctx) => {
|
|
917
|
+
const scope = await requireScope(ctx);
|
|
918
|
+
await ctx.access.node(ctx.params.id, 'read');
|
|
919
|
+
return { scope, data: await ctx.sidecar.data(ctx.params.id, scope) };
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
r.post('/api/items/:id/data', ['sidecar'], async (ctx) => {
|
|
923
|
+
const scope = await requireScope(ctx);
|
|
924
|
+
requirePrincipal(ctx.principal);
|
|
925
|
+
await ctx.access.node(ctx.params.id, 'write');
|
|
926
|
+
const b = await body(ctx.req);
|
|
927
|
+
// A BATCH, because the client flushes what it queued while offline and one request
|
|
928
|
+
// per key would make a reconnect a thundering herd against one sidecar document.
|
|
929
|
+
const entries = Array.isArray(b.entries) ? b.entries : [{ key: b.key, value: b.value, remove: b.remove }];
|
|
930
|
+
for (const e of entries) {
|
|
931
|
+
if (!e?.key) continue;
|
|
932
|
+
if (e.remove) await ctx.sidecar.removeData(ctx.params.id, scope, e.key, ctx.principal);
|
|
933
|
+
else await ctx.sidecar.setData(ctx.params.id, scope, e.key, e.value, ctx.principal);
|
|
934
|
+
}
|
|
935
|
+
// The merged view back, so a client that was offline learns what won without a
|
|
936
|
+
// second round trip — which is the whole shape of "write, then reconcile".
|
|
937
|
+
return { scope, data: await ctx.sidecar.data(ctx.params.id, scope) };
|
|
938
|
+
});
|
|
939
|
+
|
|
866
940
|
r.post('/api/items/:id/comments', [], async (ctx) => {
|
|
867
941
|
requirePrincipal(ctx.principal);
|
|
868
942
|
const node = await ctx.access.node(ctx.params.id, 'write');
|