backpack-viewer 0.6.0 → 0.7.1

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 (54) hide show
  1. package/bin/serve.js +159 -396
  2. package/dist/app/assets/index-D-H7agBH.js +12 -0
  3. package/dist/app/assets/index-DE73ngo-.css +1 -0
  4. package/dist/app/assets/index-Lvl7EMM_.js +6 -0
  5. package/dist/app/index.html +2 -2
  6. package/dist/bridge.d.ts +22 -0
  7. package/dist/bridge.js +41 -0
  8. package/dist/config.js +10 -0
  9. package/dist/copy-prompt.d.ts +17 -0
  10. package/dist/copy-prompt.js +81 -0
  11. package/dist/default-config.json +4 -0
  12. package/dist/dom-utils.d.ts +46 -0
  13. package/dist/dom-utils.js +57 -0
  14. package/dist/empty-state.js +63 -31
  15. package/dist/extensions/api.d.ts +15 -0
  16. package/dist/extensions/api.js +185 -0
  17. package/dist/extensions/chat/backpack-extension.json +23 -0
  18. package/dist/extensions/chat/src/index.js +32 -0
  19. package/dist/extensions/chat/src/panel.js +306 -0
  20. package/dist/extensions/chat/src/providers/anthropic.js +158 -0
  21. package/dist/extensions/chat/src/providers/types.js +15 -0
  22. package/dist/extensions/chat/src/tools.js +281 -0
  23. package/dist/extensions/chat/style.css +147 -0
  24. package/dist/extensions/event-bus.d.ts +12 -0
  25. package/dist/extensions/event-bus.js +30 -0
  26. package/dist/extensions/loader.d.ts +32 -0
  27. package/dist/extensions/loader.js +71 -0
  28. package/dist/extensions/manifest.d.ts +54 -0
  29. package/dist/extensions/manifest.js +116 -0
  30. package/dist/extensions/panel-mount.d.ts +26 -0
  31. package/dist/extensions/panel-mount.js +377 -0
  32. package/dist/extensions/share/backpack-extension.json +20 -0
  33. package/dist/extensions/share/src/index.js +357 -0
  34. package/dist/extensions/share/style.css +151 -0
  35. package/dist/extensions/taskbar.d.ts +29 -0
  36. package/dist/extensions/taskbar.js +64 -0
  37. package/dist/extensions/types.d.ts +182 -0
  38. package/dist/extensions/types.js +8 -0
  39. package/dist/info-panel.d.ts +2 -1
  40. package/dist/info-panel.js +78 -87
  41. package/dist/main.js +189 -29
  42. package/dist/search.js +1 -1
  43. package/dist/server-api-routes.d.ts +56 -0
  44. package/dist/server-api-routes.js +460 -0
  45. package/dist/server-extensions.d.ts +126 -0
  46. package/dist/server-extensions.js +272 -0
  47. package/dist/server-viewer-state.d.ts +18 -0
  48. package/dist/server-viewer-state.js +33 -0
  49. package/dist/sidebar.js +19 -7
  50. package/dist/style.css +356 -74
  51. package/dist/tools-pane.js +31 -14
  52. package/package.json +4 -3
  53. package/dist/app/assets/index-B3z5bBGl.css +0 -1
  54. package/dist/app/assets/index-BROJmzot.js +0 -35
@@ -0,0 +1,460 @@
1
+ import { listBackpacks, getActiveBackpack, setActiveBackpack, registerBackpack, unregisterBackpack, } from "backpack-ontology";
2
+ // --- Small HTTP helpers (used only inside this module) ---
3
+ function readBody(req) {
4
+ return new Promise((resolve, reject) => {
5
+ let body = "";
6
+ req.on("data", (chunk) => {
7
+ body += chunk.toString();
8
+ });
9
+ req.on("end", () => resolve(body));
10
+ req.on("error", reject);
11
+ });
12
+ }
13
+ function sendJson(res, status, value) {
14
+ res.writeHead(status, { "Content-Type": "application/json" });
15
+ res.end(JSON.stringify(value));
16
+ }
17
+ function sendErr(res, status, message) {
18
+ sendJson(res, status, { error: message });
19
+ }
20
+ function urlPath(req) {
21
+ return (req.url ?? "/").replace(/\?.*$/, "");
22
+ }
23
+ // --- Main dispatcher ---
24
+ /**
25
+ * Try to match and handle an API request. Returns true if the request
26
+ * was handled (response written), false if no route matched and the
27
+ * caller should fall through to its own handlers (e.g., static files).
28
+ *
29
+ * Errors that escape route handlers result in a 500 with a JSON body.
30
+ */
31
+ export async function handleApiRequest(req, res, ctx) {
32
+ const method = req.method ?? "GET";
33
+ const url = urlPath(req);
34
+ try {
35
+ // --- /api/config ---
36
+ if (url === "/api/config" && method === "GET") {
37
+ sendJson(res, 200, ctx.viewerConfig);
38
+ return true;
39
+ }
40
+ // --- /api/version-check ---
41
+ if (url === "/api/version-check" && method === "GET") {
42
+ const result = await ctx.versionCheck();
43
+ sendJson(res, 200, result);
44
+ return true;
45
+ }
46
+ // --- /api/remotes (read-only) ---
47
+ if (url === "/api/remotes" && method === "GET") {
48
+ const remotes = await ctx.remoteRegistry.list();
49
+ const summaries = await Promise.all(remotes.map(async (r) => {
50
+ let nodeCount = 0;
51
+ let edgeCount = 0;
52
+ try {
53
+ const data = await ctx.remoteRegistry.loadCached(r.name);
54
+ nodeCount = data.nodes.length;
55
+ edgeCount = data.edges.length;
56
+ }
57
+ catch {
58
+ /* keep counts at 0 */
59
+ }
60
+ return {
61
+ name: r.name,
62
+ url: r.url,
63
+ source: r.source,
64
+ addedAt: r.addedAt,
65
+ lastFetched: r.lastFetched,
66
+ pinned: r.pinned,
67
+ sizeBytes: r.sizeBytes,
68
+ nodeCount,
69
+ edgeCount,
70
+ };
71
+ }));
72
+ sendJson(res, 200, summaries);
73
+ return true;
74
+ }
75
+ const remoteItem = url.match(/^\/api\/remotes\/(.+)$/);
76
+ if (remoteItem && method === "GET") {
77
+ const name = decodeURIComponent(remoteItem[1]);
78
+ try {
79
+ const data = await ctx.remoteRegistry.loadCached(name);
80
+ sendJson(res, 200, data);
81
+ }
82
+ catch (err) {
83
+ sendErr(res, 404, err.message);
84
+ }
85
+ return true;
86
+ }
87
+ // --- /api/graphs/<name>/branches/* ---
88
+ const branchSwitch = url.match(/^\/api\/graphs\/(.+)\/branches\/switch$/);
89
+ if (branchSwitch && method === "POST") {
90
+ const graphName = decodeURIComponent(branchSwitch[1]);
91
+ const body = await readBody(req);
92
+ try {
93
+ const { name: branchName } = JSON.parse(body);
94
+ await ctx.storage.current.switchBranch(graphName, branchName);
95
+ sendJson(res, 200, { ok: true });
96
+ }
97
+ catch (err) {
98
+ sendErr(res, 400, err.message);
99
+ }
100
+ return true;
101
+ }
102
+ const deleteBranch = url.match(/^\/api\/graphs\/(.+)\/branches\/(.+)$/);
103
+ if (deleteBranch && method === "DELETE") {
104
+ const graphName = decodeURIComponent(deleteBranch[1]);
105
+ const branchName = decodeURIComponent(deleteBranch[2]);
106
+ try {
107
+ await ctx.storage.current.deleteBranch(graphName, branchName);
108
+ sendJson(res, 200, { ok: true });
109
+ }
110
+ catch (err) {
111
+ sendErr(res, 400, err.message);
112
+ }
113
+ return true;
114
+ }
115
+ const branches = url.match(/^\/api\/graphs\/(.+)\/branches$/);
116
+ if (branches && method === "GET") {
117
+ const graphName = decodeURIComponent(branches[1]);
118
+ try {
119
+ const list = await ctx.storage.current.listBranches(graphName);
120
+ sendJson(res, 200, list);
121
+ }
122
+ catch (err) {
123
+ sendErr(res, 500, err.message);
124
+ }
125
+ return true;
126
+ }
127
+ if (branches && method === "POST") {
128
+ const graphName = decodeURIComponent(branches[1]);
129
+ const body = await readBody(req);
130
+ try {
131
+ const { name: branchName, from } = JSON.parse(body);
132
+ await ctx.storage.current.createBranch(graphName, branchName, from);
133
+ sendJson(res, 200, { ok: true });
134
+ }
135
+ catch (err) {
136
+ sendErr(res, 400, err.message);
137
+ }
138
+ return true;
139
+ }
140
+ // --- /api/graphs/<name>/snapshots ---
141
+ const snapshots = url.match(/^\/api\/graphs\/(.+)\/snapshots$/);
142
+ if (snapshots && method === "GET") {
143
+ const graphName = decodeURIComponent(snapshots[1]);
144
+ try {
145
+ const list = await ctx.storage.current.listSnapshots(graphName);
146
+ sendJson(res, 200, list);
147
+ }
148
+ catch (err) {
149
+ sendErr(res, 500, err.message);
150
+ }
151
+ return true;
152
+ }
153
+ if (snapshots && method === "POST") {
154
+ const graphName = decodeURIComponent(snapshots[1]);
155
+ const body = await readBody(req);
156
+ try {
157
+ const { label } = JSON.parse(body);
158
+ await ctx.storage.current.createSnapshot(graphName, label);
159
+ sendJson(res, 200, { ok: true });
160
+ }
161
+ catch (err) {
162
+ sendErr(res, 400, err.message);
163
+ }
164
+ return true;
165
+ }
166
+ // --- /api/graphs/<name>/rollback ---
167
+ const rollback = url.match(/^\/api\/graphs\/(.+)\/rollback$/);
168
+ if (rollback && method === "POST") {
169
+ const graphName = decodeURIComponent(rollback[1]);
170
+ const body = await readBody(req);
171
+ try {
172
+ const { version } = JSON.parse(body);
173
+ await ctx.storage.current.rollback(graphName, version);
174
+ sendJson(res, 200, { ok: true });
175
+ }
176
+ catch (err) {
177
+ sendErr(res, 400, err.message);
178
+ }
179
+ return true;
180
+ }
181
+ // --- /api/graphs/<name>/diff/<version> ---
182
+ const diff = url.match(/^\/api\/graphs\/(.+)\/diff\/(\d+)$/);
183
+ if (diff && method === "GET") {
184
+ const graphName = decodeURIComponent(diff[1]);
185
+ const version = parseInt(diff[2], 10);
186
+ try {
187
+ const current = await ctx.storage.current.loadOntology(graphName);
188
+ const snapshot = await ctx.storage.current.loadSnapshot(graphName, version);
189
+ const currentNodeIds = new Set(current.nodes.map((n) => n.id));
190
+ const snapshotNodeIds = new Set(snapshot.nodes.map((n) => n.id));
191
+ const currentEdgeIds = new Set(current.edges.map((e) => e.id));
192
+ const snapshotEdgeIds = new Set(snapshot.edges.map((e) => e.id));
193
+ sendJson(res, 200, {
194
+ nodesAdded: current.nodes.filter((n) => !snapshotNodeIds.has(n.id)).length,
195
+ nodesRemoved: snapshot.nodes.filter((n) => !currentNodeIds.has(n.id)).length,
196
+ edgesAdded: current.edges.filter((e) => !snapshotEdgeIds.has(e.id)).length,
197
+ edgesRemoved: snapshot.edges.filter((e) => !currentEdgeIds.has(e.id)).length,
198
+ });
199
+ }
200
+ catch (err) {
201
+ sendErr(res, 500, err.message);
202
+ }
203
+ return true;
204
+ }
205
+ // --- /api/graphs/<name>/snippets/<id> ---
206
+ const snippetItem = url.match(/^\/api\/graphs\/(.+)\/snippets\/(.+)$/);
207
+ if (snippetItem && method === "GET") {
208
+ const graphName = decodeURIComponent(snippetItem[1]);
209
+ const snippetId = decodeURIComponent(snippetItem[2]);
210
+ try {
211
+ const snippet = await ctx.storage.current.loadSnippet(graphName, snippetId);
212
+ sendJson(res, 200, snippet);
213
+ }
214
+ catch {
215
+ sendErr(res, 404, "Snippet not found");
216
+ }
217
+ return true;
218
+ }
219
+ if (snippetItem && method === "DELETE") {
220
+ const graphName = decodeURIComponent(snippetItem[1]);
221
+ const snippetId = decodeURIComponent(snippetItem[2]);
222
+ try {
223
+ await ctx.storage.current.deleteSnippet(graphName, snippetId);
224
+ sendJson(res, 200, { ok: true });
225
+ }
226
+ catch (err) {
227
+ sendErr(res, 400, err.message);
228
+ }
229
+ return true;
230
+ }
231
+ const snippets = url.match(/^\/api\/graphs\/(.+)\/snippets$/);
232
+ if (snippets && method === "GET") {
233
+ const graphName = decodeURIComponent(snippets[1]);
234
+ try {
235
+ const list = await ctx.storage.current.listSnippets(graphName);
236
+ sendJson(res, 200, list);
237
+ }
238
+ catch {
239
+ sendJson(res, 200, []);
240
+ }
241
+ return true;
242
+ }
243
+ if (snippets && method === "POST") {
244
+ const graphName = decodeURIComponent(snippets[1]);
245
+ const body = await readBody(req);
246
+ try {
247
+ const { label, description, nodeIds, edgeIds } = JSON.parse(body);
248
+ const id = await ctx.storage.current.saveSnippet(graphName, {
249
+ label,
250
+ description,
251
+ nodeIds,
252
+ edgeIds: edgeIds ?? [],
253
+ });
254
+ sendJson(res, 200, { ok: true, id });
255
+ }
256
+ catch (err) {
257
+ sendErr(res, 400, err.message);
258
+ }
259
+ return true;
260
+ }
261
+ // --- /api/backpacks (meta: list, active, register, switch, unregister) ---
262
+ if (url === "/api/backpacks" && method === "GET") {
263
+ try {
264
+ const list = await listBackpacks();
265
+ const active = await getActiveBackpack();
266
+ sendJson(res, 200, list.map((b) => ({ ...b, active: b.name === active.name })));
267
+ }
268
+ catch (err) {
269
+ sendErr(res, 500, err.message);
270
+ }
271
+ return true;
272
+ }
273
+ if (url === "/api/backpacks/active" && method === "GET") {
274
+ try {
275
+ const active = await getActiveBackpack();
276
+ sendJson(res, 200, active);
277
+ }
278
+ catch (err) {
279
+ sendErr(res, 500, err.message);
280
+ }
281
+ return true;
282
+ }
283
+ if (url === "/api/backpacks/switch" && method === "POST") {
284
+ const body = await readBody(req);
285
+ try {
286
+ const { name } = JSON.parse(body);
287
+ await setActiveBackpack(name);
288
+ const swapped = await ctx.makeBackend();
289
+ ctx.storage.current = swapped.backend;
290
+ ctx.storage.activeEntry = swapped.entry;
291
+ ctx.onActiveBackpackChange?.();
292
+ sendJson(res, 200, { ok: true, active: ctx.storage.activeEntry });
293
+ }
294
+ catch (err) {
295
+ sendErr(res, 400, err.message);
296
+ }
297
+ return true;
298
+ }
299
+ if (url === "/api/backpacks" && method === "POST") {
300
+ const body = await readBody(req);
301
+ try {
302
+ const { name, path: p, activate } = JSON.parse(body);
303
+ // registerBackpack only takes a path; the name is derived by
304
+ // backpack-ontology from the directory. Pre-existing call sites
305
+ // pass `name` as a hint but it's not used by the function.
306
+ void name;
307
+ const entry = await registerBackpack(p);
308
+ if (activate) {
309
+ await setActiveBackpack(name);
310
+ const swapped = await ctx.makeBackend();
311
+ ctx.storage.current = swapped.backend;
312
+ ctx.storage.activeEntry = swapped.entry;
313
+ ctx.onActiveBackpackChange?.();
314
+ }
315
+ sendJson(res, 200, { ok: true, entry });
316
+ }
317
+ catch (err) {
318
+ sendErr(res, 400, err.message);
319
+ }
320
+ return true;
321
+ }
322
+ const backpackDelete = url.match(/^\/api\/backpacks\/(.+)$/);
323
+ if (backpackDelete && method === "DELETE") {
324
+ const name = decodeURIComponent(backpackDelete[1]);
325
+ try {
326
+ await unregisterBackpack(name);
327
+ if (ctx.storage.activeEntry && ctx.storage.activeEntry.name === name) {
328
+ const swapped = await ctx.makeBackend();
329
+ ctx.storage.current = swapped.backend;
330
+ ctx.storage.activeEntry = swapped.entry;
331
+ ctx.onActiveBackpackChange?.();
332
+ }
333
+ sendJson(res, 200, { ok: true });
334
+ }
335
+ catch (err) {
336
+ sendErr(res, 400, err.message);
337
+ }
338
+ return true;
339
+ }
340
+ // --- /api/locks ---
341
+ if (url === "/api/locks" && method === "GET") {
342
+ try {
343
+ const summaries = await ctx.storage.current.listOntologies();
344
+ const result = {};
345
+ const storage = ctx.storage.current;
346
+ if (typeof storage.readLock === "function") {
347
+ await Promise.all(summaries.map(async (s) => {
348
+ try {
349
+ result[s.name] = await storage.readLock(s.name);
350
+ }
351
+ catch {
352
+ result[s.name] = null;
353
+ }
354
+ }));
355
+ }
356
+ sendJson(res, 200, result);
357
+ }
358
+ catch {
359
+ sendJson(res, 200, {});
360
+ }
361
+ return true;
362
+ }
363
+ const lock = url.match(/^\/api\/graphs\/(.+)\/lock$/);
364
+ if (lock && method === "GET") {
365
+ const graphName = decodeURIComponent(lock[1]);
366
+ try {
367
+ const storage = ctx.storage.current;
368
+ const lockInfo = typeof storage.readLock === "function"
369
+ ? await storage.readLock(graphName)
370
+ : null;
371
+ sendJson(res, 200, lockInfo);
372
+ }
373
+ catch {
374
+ sendJson(res, 200, null);
375
+ }
376
+ return true;
377
+ }
378
+ // --- /api/ontologies/* ---
379
+ if (url === "/api/ontologies" && method === "GET") {
380
+ try {
381
+ const summaries = await ctx.storage.current.listOntologies();
382
+ sendJson(res, 200, summaries);
383
+ }
384
+ catch {
385
+ sendJson(res, 200, []);
386
+ }
387
+ return true;
388
+ }
389
+ // /api/ontologies/<name>/rename — must match before /api/ontologies/<name>
390
+ const rename = url.match(/^\/api\/ontologies\/(.+)\/rename$/);
391
+ if (rename && method === "POST") {
392
+ const oldName = decodeURIComponent(rename[1]);
393
+ const body = await readBody(req);
394
+ try {
395
+ const { name: newName } = JSON.parse(body);
396
+ await ctx.storage.current.renameOntology(oldName, newName);
397
+ sendJson(res, 200, { ok: true, name: newName });
398
+ }
399
+ catch (err) {
400
+ sendErr(res, 500, err.message);
401
+ }
402
+ return true;
403
+ }
404
+ if (url.startsWith("/api/ontologies/")) {
405
+ const name = decodeURIComponent(url.replace("/api/ontologies/", ""));
406
+ if (!name)
407
+ return false;
408
+ if (method === "PUT") {
409
+ const body = await readBody(req);
410
+ try {
411
+ const data = JSON.parse(body);
412
+ await ctx.storage.current.saveOntology(name, data);
413
+ sendJson(res, 200, { ok: true });
414
+ }
415
+ catch (err) {
416
+ sendErr(res, 500, err.message);
417
+ }
418
+ return true;
419
+ }
420
+ if (method === "GET") {
421
+ try {
422
+ const data = await ctx.storage.current.loadOntology(name);
423
+ sendJson(res, 200, data);
424
+ }
425
+ catch {
426
+ sendErr(res, 404, "Ontology not found");
427
+ }
428
+ return true;
429
+ }
430
+ }
431
+ // --- /oauth/callback (for Share extension OAuth popup) ---
432
+ if (url.startsWith("/oauth/callback") && method === "GET") {
433
+ res.writeHead(200, { "Content-Type": "text/html" });
434
+ res.end(`<!DOCTYPE html><html><body><script>
435
+ var params = new URLSearchParams(window.location.search);
436
+ var code = params.get("code");
437
+ var state = params.get("state");
438
+ if (window.opener && code) {
439
+ window.opener.postMessage({
440
+ type: "backpack-oauth-callback",
441
+ code: code,
442
+ returnedState: state
443
+ }, "*");
444
+ }
445
+ window.close();
446
+ </script></body></html>`);
447
+ return true;
448
+ }
449
+ return false;
450
+ }
451
+ catch (err) {
452
+ if (!res.headersSent) {
453
+ sendErr(res, 500, err.message);
454
+ }
455
+ else {
456
+ res.end();
457
+ }
458
+ return true;
459
+ }
460
+ }
@@ -0,0 +1,126 @@
1
+ import { configDir, dataDir } from "backpack-ontology";
2
+ import { type ExtensionManifest } from "./extensions/manifest.js";
3
+ /**
4
+ * Server-side extension infrastructure shared by `bin/serve.js` (prod)
5
+ * and `vite.config.ts` (dev). Both entry files import these helpers and
6
+ * keep only thin HTTP wiring.
7
+ *
8
+ * Responsibilities:
9
+ * - Resolve enabled extensions from config + first-party in-tree dirs
10
+ * - Parse and validate each manifest
11
+ * - Provide handlers for the generic extension endpoints:
12
+ * GET /api/extensions
13
+ * POST /api/extensions/<name>/fetch
14
+ * GET /api/extensions/<name>/settings
15
+ * PUT /api/extensions/<name>/settings/<key>
16
+ * DELETE /api/extensions/<name>/settings/<key>
17
+ *
18
+ * Static file serving (`/extensions/<name>/<file>`) is handled by each
19
+ * entry file because the static-file plumbing differs between Vite and
20
+ * the raw http server.
21
+ */
22
+ export interface LoadedExtension {
23
+ manifest: ExtensionManifest;
24
+ /** Absolute path to the extension's directory on disk. */
25
+ rootDir: string;
26
+ /** Whether this extension shipped with the viewer (in-tree). */
27
+ firstParty: boolean;
28
+ }
29
+ export interface ExtensionConfigEntry {
30
+ /** Extension name (must match the manifest and the directory). */
31
+ name: string;
32
+ /**
33
+ * Source. Either:
34
+ * - "first-party" — load from <viewer-dist>/extensions/<name> or
35
+ * <viewer-src>/extensions/<name> in dev
36
+ * - { path: "/abs/path" } — load from a user-specified absolute path
37
+ */
38
+ source: "first-party" | {
39
+ path: string;
40
+ };
41
+ enabled?: boolean;
42
+ }
43
+ /**
44
+ * Resolve enabled extensions. Reads:
45
+ * 1. The list of first-party extensions bundled with the viewer
46
+ * (default-enabled unless the user disabled them)
47
+ * 2. The list of external extensions from the user's viewer config
48
+ *
49
+ * For each one: locates the manifest, validates it, and returns the
50
+ * loaded extension. Errors are logged but don't crash the server —
51
+ * a malformed extension is skipped, others keep working.
52
+ *
53
+ * @param firstPartyDir absolute path to the directory containing
54
+ * bundled first-party extensions (typically
55
+ * `<dist>/extensions/` in prod or
56
+ * `<repo>/extensions/` in dev)
57
+ * @param userExtensions list from viewer config; same shape regardless of source
58
+ * @param disabledFirstParty names of first-party extensions the user disabled
59
+ */
60
+ export declare function loadExtensions(firstPartyDir: string, userExtensions: {
61
+ name: string;
62
+ path: string;
63
+ }[], disabledFirstParty: Set<string>): LoadedExtension[];
64
+ /**
65
+ * Per-extension settings file path. Each extension gets its own file
66
+ * under `~/.config/backpack/extensions/<name>/settings.json`. The file
67
+ * is created on first write.
68
+ */
69
+ export declare function extensionSettingsPath(extName: string): string;
70
+ export declare function readExtensionSettings(extName: string): Promise<Record<string, unknown>>;
71
+ export declare function writeExtensionSetting(extName: string, key: string, value: unknown): Promise<void>;
72
+ export declare function deleteExtensionSetting(extName: string, key: string): Promise<void>;
73
+ /**
74
+ * Result of an extension fetch proxy call. Either an error JSON to send
75
+ * back to the client, or an upstream body for the caller to pipe through.
76
+ */
77
+ export interface ExtensionFetchResult {
78
+ status: number;
79
+ errorJson?: string;
80
+ upstreamHeaders?: Headers;
81
+ upstreamBody?: ReadableStream<Uint8Array>;
82
+ }
83
+ /**
84
+ * Forward an extension fetch request to its declared upstream.
85
+ *
86
+ * Validates that the requested URL's origin is in the extension's
87
+ * manifest network allowlist. Injects headers (env-var or literal) per
88
+ * the manifest's `injectHeaders` config. Returns either a structured
89
+ * error or a ReadableStream the caller pipes back to the browser.
90
+ *
91
+ * The browser-side `viewer.fetch(url, init)` POSTs to
92
+ * `/api/extensions/<name>/fetch` with a JSON body matching
93
+ * ProxyRequestPayload. The caller (serve.js / vite plugin) handles the
94
+ * raw HTTP wiring; this function handles validation, secret injection,
95
+ * and the upstream call.
96
+ */
97
+ export declare function proxyExtensionFetch(ext: LoadedExtension, rawJsonBody: string): Promise<ExtensionFetchResult>;
98
+ /**
99
+ * Look up a loaded extension by name. Used by the per-extension
100
+ * endpoints to resolve which extension a request is targeting.
101
+ */
102
+ export declare function findExtension(extensions: LoadedExtension[], name: string): LoadedExtension | null;
103
+ /**
104
+ * Public manifest serialization — what `GET /api/extensions` returns.
105
+ * Strips no fields currently but exists as the boundary in case we want
106
+ * to filter (e.g., redact env-var names) in the future.
107
+ */
108
+ export declare function publicExtensionInfo(ext: LoadedExtension): {
109
+ name: string;
110
+ version: string;
111
+ viewerApi: string;
112
+ displayName: string | undefined;
113
+ description: string | undefined;
114
+ entry: string;
115
+ stylesheet: string | undefined;
116
+ permissions: import("./extensions/manifest.js").ManifestPermissions | undefined;
117
+ firstParty: boolean;
118
+ };
119
+ /**
120
+ * Resolve the on-disk file path for a request like
121
+ * `/extensions/<name>/<sub-path>`. Validates the sub-path stays within
122
+ * the extension's root dir (no path traversal). Returns null if the
123
+ * extension is unknown or the path escapes.
124
+ */
125
+ export declare function resolveExtensionFile(extensions: LoadedExtension[], extName: string, subPath: string): string | null;
126
+ export { configDir, dataDir };