@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,316 @@
1
+ // Injectable browser build of the Trove plugin SDK — a single self-contained IIFE
2
+ // with NO imports, so the host can inline it into a sandboxed iframe's srcdoc
3
+ // alongside the plugin's entry script. The iframe runs on an opaque origin
4
+ // (sandbox="allow-scripts", no allow-same-origin), so it can't fetch its own
5
+ // package files; instead it reaches everything — resources, files, settings,
6
+ // storage — through the host over a transferred MessagePort. Package resources
7
+ // are handed back as raw bytes (or iframe-local blob: URLs), never host URLs, so
8
+ // the plugin only ever holds opaque handles.
9
+ //
10
+ // Plugins use it as: trove.activate(async (ctx) => { ... })
11
+ (function () {
12
+ 'use strict';
13
+ // Wire-protocol version this SDK speaks. MUST equal PROTOCOL_VERSION in
14
+ // protocol.js — this file is injected as text and cannot import it, so
15
+ // protocol.test.js asserts the two stay in step.
16
+ const SDK_PROTOCOL_VERSION = '1.0';
17
+ let port = null, manifest = null, capabilities = [], storageScopes = {}, online = true, seq = 0, role = 'primary';
18
+ const pending = new Map();
19
+ const commandHandlers = new Map();
20
+ const openerHandlers = new Map();
21
+ let onConnectivity = null, onDeactivate = null, onSettingsChange = null, onDock = null;
22
+ const mediaHandlers = {}; // action -> handler, for OS media-session controls
23
+
24
+ const now = () => { try { return Date.now(); } catch { return 0; } };
25
+
26
+ // The HOST times its own calls out; this side did not, and `pending` was never
27
+ // rejected — not on a dropped reply, not on port close. A plugin awaiting one hung
28
+ // forever with no way to find out, and the entry leaked with it.
29
+ const CALL_TIMEOUT_MS = 30_000;
30
+
31
+ function call(method, params, transfer) {
32
+ const id = ++seq;
33
+ return new Promise((resolve, reject) => {
34
+ const timer = setTimeout(() => {
35
+ pending.delete(id);
36
+ reject(new Error(`Timed out waiting for the host to answer "${method}"`));
37
+ }, CALL_TIMEOUT_MS);
38
+ if (timer && timer.unref) timer.unref();
39
+ pending.set(id, { resolve, reject, timer });
40
+ port.postMessage({ __trove: 'req', id, method, params }, transfer || []);
41
+ });
42
+ }
43
+ const emit = (method, params) => port.postMessage({ __trove: 'event', method, params });
44
+
45
+ // What this frame reports about itself on every heartbeat. Contributions are the
46
+ // host's own manifest reading — all the plugin can usefully say is which of its
47
+ // declared contributions it actually bound a handler to, plus whether it thinks
48
+ // it's online.
49
+ function buildManifest() {
50
+ return {
51
+ domain: manifest && manifest.domain, name: manifest && manifest.name,
52
+ online, role, ts: now(),
53
+ handlers: [...commandHandlers.keys()],
54
+ };
55
+ }
56
+ const announce = () => port && emit('manifest', buildManifest());
57
+
58
+ function onPort(e) {
59
+ const m = e.data;
60
+ if (!m || m.__trove == null) return;
61
+ if (m.__trove === 'res') {
62
+ const p = pending.get(m.id);
63
+ if (!p) return;
64
+ clearTimeout(p.timer);
65
+ pending.delete(m.id);
66
+ m.error ? p.reject(Object.assign(new Error(m.error.message), m.error)) : p.resolve(m.result);
67
+ } else if (m.__trove === 'req') {
68
+ Promise.resolve().then(() => dispatch(m.method, m.params))
69
+ .then((result) => port.postMessage({ __trove: 'res', id: m.id, result }))
70
+ .catch((err) => port.postMessage({ __trove: 'res', id: m.id, error: { message: err.message } }));
71
+ } else if (m.__trove === 'event') {
72
+ dispatchEvent(m.method, m.params);
73
+ }
74
+ }
75
+
76
+ function dispatch(method, params) {
77
+ if (method === 'command:execute') {
78
+ const h = commandHandlers.get(params.id);
79
+ // Throw, like `opener:open` two lines down. Resolving `undefined` for an id with
80
+ // no handler told the host the command had RUN when nothing had.
81
+ if (!h) throw new Error(`No handler registered for command "${params.id}"`);
82
+ return h(...(params.args || []));
83
+ }
84
+ if (method === 'opener:open') {
85
+ // An opener frame boots at that opener's entry module and runs exactly one
86
+ // opener, so an unkeyed onOpen(fn) handler is the normal case.
87
+ const f = openerHandlers.get(params.openerId) || openerHandlers.get('*');
88
+ if (!f) throw new Error('no opener ' + params.openerId);
89
+ return f(params.file, params.context);
90
+ }
91
+ if (method === 'manifest') return buildManifest();
92
+ throw new Error('Unknown host call ' + method);
93
+ }
94
+ async function dispatchEvent(method, params) {
95
+ if (method === 'deactivate') return onDeactivate && onDeactivate();
96
+ if (method === 'connectivity') { online = !!params.online; try { onConnectivity && (await onConnectivity({ online })); } catch (e) { console.error(e); } announce(); }
97
+ if (method === 'settings:changed') { try { onSettingsChange && onSettingsChange(params.key, params.value); } catch (e) { console.error(e); } }
98
+ // The OS/host fired a media transport control (play/pause/next/seek…).
99
+ if (method === 'media:action') { try { mediaHandlers[params.action] && mediaHandlers[params.action](params); } catch (e) { console.error(e); } }
100
+ // The host docked or undocked this viewer (see ctx.dock).
101
+ if (method === 'dock:state') { try { onDock && onDock(params); } catch (e) { console.error(e); } }
102
+ }
103
+
104
+ function requireCap(cap) { if (!capabilities.includes(cap)) throw new Error('Plugin lacks capability "' + cap + '"'); }
105
+
106
+ // Storage: one async SQL handle (mirrors the host SqliteDatabase interface) per
107
+ // scope+side, over RPC. Only granted scopes are exposed on ctx.storage.
108
+ function sqlHandle(scope, side) {
109
+ var send = function (op, extra) { return call('storage:sql', Object.assign({ scope: scope, side: side, op: op }, extra)); };
110
+ return {
111
+ exec: function (sql) { return send('exec', { sql: sql }); },
112
+ run: function (sql) { return send('run', { sql: sql, params: [].slice.call(arguments, 1) }); },
113
+ get: function (sql) { return send('get', { sql: sql, params: [].slice.call(arguments, 1) }); },
114
+ all: function (sql) { return send('all', { sql: sql, params: [].slice.call(arguments, 1) }); },
115
+ batch: function (statements) { return send('batch', { statements: statements }); },
116
+ };
117
+ }
118
+ function scopeHandle(scope) {
119
+ return { server: sqlHandle(scope, 'server'), client: sqlHandle(scope, 'client') };
120
+ }
121
+ function makeStorage() {
122
+ var s = {};
123
+ if (storageScopes.plugin) s.plugin = scopeHandle('plugin');
124
+ if (storageScopes.domain) s.domain = scopeHandle('domain');
125
+ return s;
126
+ }
127
+
128
+ function hasHeader(h, name) { name = name.toLowerCase(); for (var k in h) if (k.toLowerCase() === name) return true; return false; }
129
+ // Wrap the host's brokered-fetch result in a minimal Response-like object.
130
+ function makeResponse(r) {
131
+ var bytes = new Uint8Array(r.bytes || new ArrayBuffer(0));
132
+ var decode = function () { return new TextDecoder().decode(bytes); };
133
+ return {
134
+ ok: r.ok, status: r.status, statusText: r.statusText, url: r.url, headers: r.headers || {},
135
+ arrayBuffer: function () { return Promise.resolve(bytes.slice().buffer); },
136
+ bytes: function () { return Promise.resolve(bytes); },
137
+ text: function () { return Promise.resolve(decode()); },
138
+ json: function () { return Promise.resolve(JSON.parse(decode())); },
139
+ };
140
+ }
141
+
142
+ function makeContext() {
143
+ return {
144
+ manifest, capabilities,
145
+ // Which instance this is: 'primary' is the plugin's single background frame
146
+ // (register commands/indexers, do one-time setup here); 'viewer' is a
147
+ // per-open frame hosting an opener for one file (drive media/dock from here).
148
+ role,
149
+ get online() { return online; },
150
+ // Contributions are DECLARED IN THE MANIFEST (openers, indexers, commands,
151
+ // statusItems, keybindings). The host registers them before this code runs, so
152
+ // a plugin never registers anything at runtime — it only supplies the behaviour
153
+ // for what it declared, addressed by id.
154
+ commands: {
155
+ /** Implement a command this plugin's manifest declares, by its short name. */
156
+ handle(name, handler) { commandHandlers.set(name, handler); return this; },
157
+ /**
158
+ * Run a command. Its OWN commands by short name; anyone else's by full address
159
+ * (a built-in like 'explorer.download', or a `trove+contrib:` URI) — and only
160
+ * if the manifest's `commands` capability lists it.
161
+ */
162
+ execute(id) { const a = [].slice.call(arguments, 1); return call('command:execute', { id, args: a }); },
163
+ },
164
+ /** Implement the opener this frame was booted for (its entry module). The id is
165
+ * optional — an opener frame runs exactly one opener. */
166
+ onOpen(idOrHandler, maybeHandler) {
167
+ const [id, handler] = typeof idOrHandler === 'function' ? ['*', idOrHandler] : [idOrHandler, maybeHandler];
168
+ openerHandlers.set(id, handler);
169
+ return this;
170
+ },
171
+ // NOTE: there is no onIndex(). Indexers run on the SERVER (in its isolate
172
+ // runtime), not in this sandbox — indexing has to happen once per upload for the
173
+ // drive, not in whichever tab is open. An indexer's entry module is plain ESM
174
+ // exporting `index(node, ctx)`; it doesn't use this SDK at all. What a plugin can
175
+ // do from here is PUSH contributions for a node via ctx.files.index (the
176
+ // `indexer` capability).
177
+ // Package resources — opaque handles. read() copies bytes into the iframe;
178
+ // url() wraps them in an iframe-local blob: URL.
179
+ resources: {
180
+ list() { return call('resources:list', {}); },
181
+ async read(path) { const r = await call('resources:read', { path }); return new Uint8Array(r.bytes); },
182
+ async text(path) { return new TextDecoder().decode(await this.read(path)); },
183
+ async url(path, type) {
184
+ const bytes = await this.read(path);
185
+ return URL.createObjectURL(new Blob([bytes], { type: type || 'application/octet-stream' }));
186
+ },
187
+ },
188
+ ui: {
189
+ toast: (text, opts) => emit('ui:toast', Object.assign({ text }, opts)),
190
+ showPanel: () => call('ui:showPanel', {}),
191
+ setBadge: (text) => emit('ui:badge', { text }),
192
+ /**
193
+ * Drive a status-bar slot this plugin's manifest declares. `html` is sanitized
194
+ * by the host down to a small inline-formatting allowlist before it renders.
195
+ * ctx.ui.status('sync').set('<b>3</b> queued')
196
+ * ctx.ui.status('sync').hide()
197
+ */
198
+ status(name) {
199
+ return {
200
+ set: (html, opts) => call('ui:status', Object.assign({ name, html, visible: true }, opts || {})),
201
+ show: () => call('ui:status', { name, visible: true }),
202
+ hide: () => call('ui:status', { name, visible: false }),
203
+ };
204
+ },
205
+ },
206
+ /**
207
+ * Registers — context value slots this plugin's manifest declares, which
208
+ * when-clauses (its own keymap's, its commands') read by contribution URI.
209
+ * ctx.registers.set('busy', true)
210
+ */
211
+ registers: {
212
+ set: (name, value) => call('context:setRegister', { name, value }),
213
+ },
214
+ // Media session — surfaces this viewer's playback to the OS (lock-screen /
215
+ // notification transport controls, so phones can play/pause/seek). The host
216
+ // owns navigator.mediaSession; action handlers are called back over RPC.
217
+ media: {
218
+ setMetadata: (m) => (requireCap('media'), call('media:metadata', m || {})),
219
+ setPlaybackState: (state) => (requireCap('media'), call('media:playbackState', { state })),
220
+ setPositionState: (p) => (requireCap('media'), call('media:position', p || {})),
221
+ setActionHandler: (action, handler) => { requireCap('media'); if (handler) mediaHandlers[action] = handler; else delete mediaHandlers[action]; return call('media:action', { action, on: !!handler }); },
222
+ clear: () => call('media:clear', {}),
223
+ },
224
+ // Dock — register this viewer to persist as a small floating frame when the
225
+ // user navigates away (a docked video = picture-in-picture; docked audio = a
226
+ // mini transport). Enable while playing/active, disable otherwise. `minSize`/
227
+ // `maxSize` are {width,height} constraints. onDock is notified on (un)dock.
228
+ dock: {
229
+ enable: (opts) => (requireCap('dock'), call('dock:enable', opts || {})),
230
+ disable: () => (requireCap('dock'), call('dock:disable', {})),
231
+ close: () => call('dock:close', {}),
232
+ onChange: (fn) => { onDock = fn; },
233
+ },
234
+ // Network — there is no direct fetch in the sandbox; the host performs the
235
+ // request, but ONLY to endpoints declared in the manifest's `network` list
236
+ // (and only with the "network" capability). Returns a Response-like object.
237
+ net: {
238
+ fetch(url, opts) {
239
+ requireCap('network');
240
+ opts = opts || {};
241
+ var body = opts.body;
242
+ var headers = Object.assign({}, opts.headers);
243
+ if (body && typeof body === 'object' && !(body instanceof ArrayBuffer) && !(body instanceof Uint8Array)) {
244
+ body = JSON.stringify(body);
245
+ if (!hasHeader(headers, 'content-type')) headers['Content-Type'] = 'application/json';
246
+ }
247
+ // `.buffer` ignores byteOffset/byteLength, so any view produced by `subarray` or
248
+ // `slice` — or a view into a pooled buffer — sent the WHOLE backing store: the wrong
249
+ // payload, and an out-of-band leak of whatever else was in it.
250
+ if (ArrayBuffer.isView(body)) {
251
+ body = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
252
+ }
253
+ return call('net:fetch', { url: String(url), method: opts.method || 'GET', headers: headers, body: body })
254
+ .then(makeResponse);
255
+ },
256
+ },
257
+ files: {
258
+ read: (id, opts) => (requireCap('files'), call('files:read', Object.assign({ id }, opts))),
259
+ list: (pathOrId, opts) => (requireCap('files'), call('files:list', Object.assign({ pathOrId }, opts))),
260
+ stat: (id) => (requireCap('files'), call('files:stat', { id })),
261
+ downloadUrl: (id) => (requireCap('files'), call('files:downloadUrl', { id })),
262
+ // index(indexerId, nodeId, contribution) where contribution is
263
+ // { semanticTexts?, tags?, metadata? }. Legacy (indexerId, nodeId, documents[], facet)
264
+ // is still accepted when the 3rd arg is an array of documents.
265
+ index: (indexerId, nodeId, contribution, facet) => {
266
+ requireCap('indexer');
267
+ var payload = Array.isArray(contribution) ? { documents: contribution, facet: facet } : (contribution || {});
268
+ return call('files:index', Object.assign({ indexerId: indexerId, nodeId: nodeId }, payload));
269
+ },
270
+ },
271
+ // Persistent storage: an isolated SQLite database per granted scope. `plugin`
272
+ // is private to this plugin; `domain` (verified packages only) is shared with
273
+ // the vendor's other plugins. Each scope exposes a `.server` handle (and, from
274
+ // Stage 3, an on-device `.client` handle) with the same async SQL surface.
275
+ storage: makeStorage(),
276
+ // The plugin's own settings (declared in the manifest). getSecret reads a
277
+ // secret-typed value the host stores separately.
278
+ settings: {
279
+ get: (key) => call('settings:get', { key }),
280
+ getSecret: (key) => call('settings:getSecret', { key }),
281
+ set: (key, value) => call('settings:set', { key, value }),
282
+ onChange: (fn) => { onSettingsChange = fn; },
283
+ },
284
+ onConnectivity: (fn) => { onConnectivity = fn; },
285
+ announce,
286
+ onDeactivate: (fn) => { onDeactivate = fn; },
287
+ };
288
+ }
289
+
290
+ async function activate(setup) {
291
+ await new Promise((resolve) => {
292
+ function onInit(e) {
293
+ if (!e.data || e.data.__trove !== 'init') return;
294
+ window.removeEventListener('message', onInit);
295
+ manifest = e.data.manifest; capabilities = e.data.capabilities || []; storageScopes = e.data.storage || {}; online = e.data.online != null ? e.data.online : true; role = e.data.role || 'primary';
296
+ port = e.ports[0];
297
+ port.onmessage = onPort;
298
+ resolve();
299
+ }
300
+ window.addEventListener('message', onInit);
301
+ parent.postMessage({ __trove: 'ready', protocolVersion: SDK_PROTOCOL_VERSION }, '*');
302
+ });
303
+ const ctx = makeContext();
304
+ try {
305
+ await setup(ctx);
306
+ await call('activated', { ok: true });
307
+ announce();
308
+ } catch (err) {
309
+ await call('activated', { ok: false, error: err && err.message });
310
+ throw err;
311
+ }
312
+ return ctx;
313
+ }
314
+
315
+ globalThis.trove = { activate };
316
+ })();
@@ -0,0 +1,32 @@
1
+ // @3sln/trove/plugin-sdk — the ESM entry a plugin author imports when building or
2
+ // bundling a plugin outside the sandbox:
3
+ //
4
+ // import { activate } from '@3sln/trove/plugin-sdk';
5
+ // activate(async (ctx) => {
6
+ // ctx.commands.register('hello.world', () => ctx.ui.toast('Hi from a plugin!'));
7
+ // });
8
+ //
9
+ // Inside a running Trove sandbox the host injects the SAME implementation directly
10
+ // (see ./browser.js) and exposes it as the global `trove`. To keep exactly one
11
+ // source of truth — so what an author imports can never drift from what actually
12
+ // runs — this module loads that implementation for its side effect and re-exports
13
+ // its surface. Importing is side-effect-safe: it only DEFINES the SDK (and sets
14
+ // `globalThis.trove`); nothing talks to the host until you call `activate()`.
15
+
16
+ import './browser.js';
17
+
18
+ const trove = globalThis.trove;
19
+
20
+ /**
21
+ * Entry point — call once at the top of your plugin. The host hands your callback
22
+ * a capability-scoped `ctx` (commands, resources, files, db, settings, net, ui).
23
+ * @param {(ctx: object) => (void | Promise<void>)} setup
24
+ * @returns {Promise<object>} the activated context
25
+ */
26
+ export const activate = trove.activate;
27
+
28
+ export default trove;
29
+
30
+ // The host-side RPC channel — used by the workbench that hosts plugins, not by
31
+ // plugins themselves. Re-exported here for convenience/parity with the subpath.
32
+ export { RpcChannel } from './rpc.js';
@@ -0,0 +1,59 @@
1
+ // The host ↔ plugin wire protocol, in one place.
2
+ //
3
+ // Envelope (over the transferred MessagePort):
4
+ // { __trove: 'req', id, method, params } plugin → host, expects a 'res'
5
+ // { __trove: 'res', id, result | error } host → plugin
6
+ // { __trove: 'event', method, params } either direction, fire-and-forget
7
+ // Bootstrap (over postMessage, before the port exists):
8
+ // { __trove: 'ready', protocolVersion } plugin → host
9
+ // { __trove: 'init', manifest, capabilities, storage, online, role, protocolVersion }
10
+ // { __trove: 'boot-error', error } plugin → host (module load failed)
11
+ //
12
+ // NOTE: browser.js is injected into the sandboxed frame as a TEXT blob (it must be a
13
+ // self-contained IIFE with no imports), so it cannot import this module. It declares
14
+ // its own `SDK_PROTOCOL_VERSION` constant instead, and protocol.test.js asserts the
15
+ // two stay equal — a drift guard in place of an import.
16
+
17
+ /** Bumped MAJOR when a change breaks older plugins; MINOR for additive changes. */
18
+ export const PROTOCOL_VERSION = '1.0';
19
+
20
+ export function majorOf(version) {
21
+ return String(version || '').split('.')[0] || '0';
22
+ }
23
+
24
+ /** Whether a plugin built against `version` can talk to this host. */
25
+ export function isCompatible(version) {
26
+ // An SDK older than versioning itself reports nothing — accept it (it predates the
27
+ // field and the protocol hasn't broken yet); a differing MAJOR is a hard mismatch.
28
+ if (!version) return true;
29
+ return majorOf(version) === majorOf(PROTOCOL_VERSION);
30
+ }
31
+
32
+ // There are no `contribute:*` methods: contributions are DECLARED IN THE MANIFEST and
33
+ // registered by the host before the plugin boots. A plugin can only ever drive what it
34
+ // declared (`ui:status`, `context:setRegister`), never add to it.
35
+
36
+ /** Canonical host methods a plugin may call. Grouped by namespace. */
37
+ export const METHODS = {
38
+ activated: 'activated',
39
+ // Note: 'command:execute' travels BOTH ways on the same channel — the host calls it
40
+ // to run a plugin-contributed command, and a plugin calls it (needs that command in
41
+ // its `commands` allowlist) to run someone else's. The direction disambiguates.
42
+ command: { execute: 'command:execute' },
43
+ resources: { list: 'resources:list', read: 'resources:read' },
44
+ files: { read: 'files:read', list: 'files:list', stat: 'files:stat', downloadUrl: 'files:downloadUrl', index: 'files:index' },
45
+ net: { fetch: 'net:fetch' },
46
+ storage: { sql: 'storage:sql' },
47
+ settings: { get: 'settings:get', set: 'settings:set', getSecret: 'settings:getSecret' },
48
+ ui: { showPanel: 'ui:showPanel', status: 'ui:status' },
49
+ context: { setRegister: 'context:setRegister' },
50
+ media: { metadata: 'media:metadata', playbackState: 'media:playbackState', position: 'media:position', action: 'media:action', clear: 'media:clear' },
51
+ dock: { enable: 'dock:enable', disable: 'dock:disable', close: 'dock:close' },
52
+ };
53
+
54
+ /** Events a plugin may emit to the host (fire-and-forget, no reply). */
55
+ export const EVENTS = {
56
+ manifest: 'manifest',
57
+ uiToast: 'ui:toast',
58
+ uiBadge: 'ui:badge',
59
+ };
@@ -0,0 +1,95 @@
1
+ // A minimal, symmetric JSON-RPC-ish channel over a MessagePort. Used on both
2
+ // ends of the host↔plugin boundary: the host keeps one port, the sandboxed
3
+ // iframe gets the other (transferred cross-origin at init), so neither side ever
4
+ // touches the other's globals — everything is messages. Requests get a reply;
5
+ // events are fire-and-forget. Handlers may be async and may throw; the error is
6
+ // serialized (code/message) back to the caller as a rejected promise.
7
+
8
+ export class RpcChannel {
9
+ /**
10
+ * @param {MessagePort|Window} port a MessagePort (preferred) or window
11
+ * @param {object} [opts]
12
+ * @param {(method:string, params:any) => any} [opts.onCall] handler for incoming requests
13
+ * @param {(method:string, params:any) => void} [opts.onEvent] handler for incoming events
14
+ * @param {string} [opts.targetOrigin] required when port is a Window
15
+ */
16
+ constructor(port, opts = {}) {
17
+ this.port = port;
18
+ this.onCall = opts.onCall;
19
+ this.onEvent = opts.onEvent;
20
+ this.targetOrigin = opts.targetOrigin;
21
+ this.seq = 0;
22
+ this.pending = new Map();
23
+ this._listener = (e) => this._receive(e.data, e);
24
+ if (typeof port.addEventListener === 'function') port.addEventListener('message', this._listener);
25
+ else port.onmessage = this._listener;
26
+ port.start?.();
27
+ }
28
+
29
+ _post(msg, transfer) {
30
+ if (this.targetOrigin) this.port.postMessage(msg, this.targetOrigin, transfer);
31
+ else this.port.postMessage(msg, transfer);
32
+ }
33
+
34
+ /** Call a remote method, awaiting its result. */
35
+ call(method, params, { timeout = 30000, transfer } = {}) {
36
+ const id = ++this.seq;
37
+ return new Promise((resolve, reject) => {
38
+ const timer = timeout
39
+ ? setTimeout(() => {
40
+ this.pending.delete(id);
41
+ reject(new Error(`RPC timeout: ${method}`));
42
+ }, timeout)
43
+ : null;
44
+ this.pending.set(id, { resolve, reject, timer });
45
+ this._post({ __trove: 'req', id, method, params }, transfer);
46
+ });
47
+ }
48
+
49
+ /** Fire an event; no reply expected. */
50
+ emit(method, params, transfer) {
51
+ this._post({ __trove: 'event', method, params }, transfer);
52
+ }
53
+
54
+ async _receive(msg, event) {
55
+ if (!msg || msg.__trove == null) return;
56
+ if (msg.__trove === 'res') {
57
+ const p = this.pending.get(msg.id);
58
+ if (!p) return;
59
+ this.pending.delete(msg.id);
60
+ if (p.timer) clearTimeout(p.timer);
61
+ if (msg.error) p.reject(Object.assign(new Error(msg.error.message), msg.error));
62
+ else p.resolve(msg.result);
63
+ return;
64
+ }
65
+ if (msg.__trove === 'event') {
66
+ try {
67
+ await this.onEvent?.(msg.method, msg.params, event);
68
+ } catch (err) {
69
+ console.error('rpc event handler error', err);
70
+ }
71
+ return;
72
+ }
73
+ if (msg.__trove === 'req') {
74
+ let result, error;
75
+ try {
76
+ result = await this.onCall?.(msg.method, msg.params, event);
77
+ } catch (err) {
78
+ error = { message: err?.message || String(err), code: err?.code || 'error' };
79
+ }
80
+ this._post({ __trove: 'res', id: msg.id, result, error });
81
+ }
82
+ }
83
+
84
+ dispose() {
85
+ if (typeof this.port.removeEventListener === 'function') {
86
+ this.port.removeEventListener('message', this._listener);
87
+ }
88
+ for (const p of this.pending.values()) {
89
+ if (p.timer) clearTimeout(p.timer);
90
+ p.reject(new Error('RPC channel disposed'));
91
+ }
92
+ this.pending.clear();
93
+ this.port.close?.();
94
+ }
95
+ }
@@ -0,0 +1,78 @@
1
+ // Bun adapter — the production runtime. Bun speaks Web Request/Response natively,
2
+ // so the server's `handle(request)` plugs straight into `Bun.serve` with no
3
+ // req/res conversion. Static assets are served with `Bun.file` (SPA fallback), and
4
+ // SQLite uses `bun:sqlite` (see core/sqlite-driver.js). The Node adapter
5
+ // (adapters/node.js) stays as a fully compatible alternative.
6
+ //
7
+ // TROVE_STORAGE=filesystem TROVE_FS_ROOT=./data/objects \
8
+ // TROVE_METADATA=sqlite TROVE_DB_PATH=./data/trove.db \
9
+ // bun packages/server/src/adapters/bun.js
10
+
11
+ import { readFileSync } from 'node:fs';
12
+ import { createServer, configFromEnv, warnOnOpenAccess } from '../index.js';
13
+ import { findWebDist } from './webDist.js';
14
+ import { createStaticAssets } from './staticAssets.js';
15
+
16
+ // A JWKS held in a file rather than inlined in the environment: multi-line JSON is
17
+ // awkward in env vars and shows up in `docker inspect`, while a mounted secret file
18
+ // does not. Read here rather than in configFromEnv, which has to stay loadable on
19
+ // Workers where there is no filesystem.
20
+ if (process.env.TROVE_JWT_JWKS_FILE && !process.env.TROVE_JWT_JWKS) {
21
+ process.env.TROVE_JWT_JWKS = readFileSync(process.env.TROVE_JWT_JWKS_FILE, 'utf8');
22
+ }
23
+
24
+
25
+ // TROVE_-prefixed to match every other setting; bare PORT/HOST still work, since that
26
+ // is what most platforms inject and breaking them would be gratuitous.
27
+ const PORT = Number(process.env.TROVE_PORT || process.env.PORT || 8787);
28
+ const HOST = process.env.TROVE_HOST || process.env.HOST || '0.0.0.0';
29
+
30
+ // Built web assets, if the app has been built — see webDist.js for why this is a
31
+ // resolution rather than a relative path.
32
+ const { dir: WEB_DIST, source: WEB_DIST_SOURCE } = findWebDist();
33
+
34
+ // Where to look, what to refuse and what to say about caching is shared with the Node
35
+ // adapter — see staticAssets.js. All that differs here is how a file is read.
36
+ const staticAssets = WEB_DIST && createStaticAssets({
37
+ dir: WEB_DIST,
38
+ read: async (filePath) => {
39
+ const file = Bun.file(filePath);
40
+ if (!(await file.exists())) return null;
41
+ return { size: file.size, mtime: file.lastModified, type: file.type, open: () => file };
42
+ },
43
+ });
44
+
45
+ const hasWeb = !!WEB_DIST;
46
+ const envConfig = configFromEnv();
47
+ warnOnOpenAccess(envConfig);
48
+ const { handle, close } = await createServer({
49
+ ...envConfig,
50
+ assets: hasWeb ? staticAssets : undefined,
51
+ });
52
+
53
+ const server = Bun.serve({
54
+ port: PORT,
55
+ hostname: HOST,
56
+ async fetch(req) {
57
+ return (await handle(req)) ?? new Response('Not found', { status: 404 });
58
+ },
59
+ error(err) {
60
+ console.error('server error', err);
61
+ return new Response('Internal error', { status: 500 });
62
+ },
63
+ });
64
+
65
+ console.log(`Trove server (Bun) on http://${HOST}:${PORT} (web assets: ${hasWeb ? WEB_DIST : `none — ${WEB_DIST_SOURCE}; run npm run build:web`})`);
66
+
67
+ // Graceful shutdown: stop serving, then flush notifications, dispose the sidecar,
68
+ // and close SQLite cleanly so a redeploy doesn't lose in-flight work.
69
+ let shuttingDown = false;
70
+ async function shutdown(signal) {
71
+ if (shuttingDown) return;
72
+ shuttingDown = true;
73
+ console.log(`Trove shutting down (${signal})…`);
74
+ try { server.stop(); } catch { /* ignore */ }
75
+ try { await close(); } catch (err) { console.error('shutdown error', err); }
76
+ process.exit(0);
77
+ }
78
+ for (const sig of ['SIGTERM', 'SIGINT']) process.on(sig, () => shutdown(sig));