Theseus: built-in extensions can answer paths under a name they ship with
Pithos needs its full-page app at pithos.sia/<user>/<drive>/<folder> instead of a loopback address. A new site-route capability lets a built-in add-on declare names in its manifest (siteRoutes) and register a handler for them; the bns:// handler asks it first for every path but the root, and a handler that returns nothing hands the request back to the name's own site. Community add-ons cannot use it, since answering for a name is impersonating it.
This commit is contained in:
parent
556a22b062
commit
bdc8f951c6
2 changed files with 60 additions and 3 deletions
|
|
@ -84,6 +84,15 @@ const KNOWN_CAPABILITIES = new Set([
|
||||||
// declare "sidebar-panel" typically follow up with
|
// declare "sidebar-panel" typically follow up with
|
||||||
// api.revealSidebar(panelId) to surface the result.
|
// api.revealSidebar(panelId) to surface the result.
|
||||||
"context-menu-item",
|
"context-menu-item",
|
||||||
|
// site-route: manifest.siteRoutes = ["pithos.sia"] + api.registerSiteRoute
|
||||||
|
// ({ host, handle }) — the add-on answers requests for paths
|
||||||
|
// under a BCNR name it ships with (Pithos serves its app at
|
||||||
|
// pithos.sia/<user>/<drive>/…). handle(request) gets the
|
||||||
|
// Fetch Request and returns a Response, or null for "not
|
||||||
|
// mine" (the name's own site then answers). The root page
|
||||||
|
// always stays the site's. Built-in add-ons only: answering
|
||||||
|
// for a name is impersonating it.
|
||||||
|
"site-route",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Chrome-style match pattern → predicate. "<scheme>://<host>/<path>" where
|
// Chrome-style match pattern → predicate. "<scheme>://<host>/<path>" where
|
||||||
|
|
@ -205,6 +214,17 @@ function validateManifest(raw, folderName) {
|
||||||
// one. Each entry is validated as an id itself and gates vault.derive by
|
// one. Each entry is validated as an id itself and gates vault.derive by
|
||||||
// (own id OR one of these) in makeApi below.
|
// (own id OR one of these) in makeApi below.
|
||||||
const absorbs = Array.isArray(m.absorbs) ? m.absorbs.map(String).filter(Boolean) : [];
|
const absorbs = Array.isArray(m.absorbs) ? m.absorbs.map(String).filter(Boolean) : [];
|
||||||
|
// siteRoutes: BCNR names this add-on answers paths under (see site-route).
|
||||||
|
const siteRoutes = [];
|
||||||
|
if (capabilities.includes("site-route")) {
|
||||||
|
const list = Array.isArray(m.siteRoutes) ? m.siteRoutes : [];
|
||||||
|
for (const h of list) {
|
||||||
|
const host = String(h || "").trim().toLowerCase();
|
||||||
|
if (!/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(host)) throw new Error(`addon "${id}": siteRoutes entry "${h}" is not a host name`);
|
||||||
|
siteRoutes.push(host);
|
||||||
|
}
|
||||||
|
if (!siteRoutes.length) throw new Error(`addon "${id}": capability "site-route" needs a non-empty "siteRoutes" list`);
|
||||||
|
}
|
||||||
for (const a of absorbs) {
|
for (const a of absorbs) {
|
||||||
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(a)) {
|
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(a)) {
|
||||||
throw new Error(`addon "${id}": absorbs entry "${a}" is not a valid add-on id`);
|
throw new Error(`addon "${id}": absorbs entry "${a}" is not a valid add-on id`);
|
||||||
|
|
@ -250,9 +270,9 @@ function validateManifest(raw, folderName) {
|
||||||
let activationNote = "";
|
let activationNote = "";
|
||||||
if (activation === "on-demand") {
|
if (activation === "on-demand") {
|
||||||
if (capabilities.includes("request-filter")) { activation = "startup"; activationNote = "request-filter add-ons start at launch"; }
|
if (capabilities.includes("request-filter")) { activation = "startup"; activationNote = "request-filter add-ons start at launch"; }
|
||||||
else if (!panels.length && !toolbarMenu && !contextMenuItems.length && !pageInject) { activation = "startup"; activationNote = "declares nothing that could start it"; }
|
else if (!panels.length && !toolbarMenu && !contextMenuItems.length && !pageInject && !siteRoutes.length) { activation = "startup"; activationNote = "declares nothing that could start it"; }
|
||||||
}
|
}
|
||||||
return { id, name, version, description, author, icon, main, capabilities, pageInject, toolbarMenu, contextMenuItems, absorbs, category, dock, panels, activation, activationNote };
|
return { id, name, version, description, author, icon, main, capabilities, pageInject, toolbarMenu, contextMenuItems, absorbs, category, dock, panels, siteRoutes, activation, activationNote };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loader singleton. `discoverAndActivate(opts)` returns a snapshot the rest
|
// Loader singleton. `discoverAndActivate(opts)` returns a snapshot the rest
|
||||||
|
|
@ -513,6 +533,19 @@ class AddonHost {
|
||||||
return Promise.all([...this._dormant.keys()].map((id) => this.ensureActive(id, reason).catch(() => {})));
|
return Promise.all([...this._dormant.keys()].map((id) => this.ensureActive(id, reason).catch(() => {})));
|
||||||
}
|
}
|
||||||
isDormant(id) { return this._dormant.has(id); }
|
isDormant(id) { return this._dormant.has(id); }
|
||||||
|
// The site route answering paths under `host`, starting its add-on if it
|
||||||
|
// waits for first use; null when no built-in add-on claims the name.
|
||||||
|
async siteRouteFor(host) {
|
||||||
|
const h = String(host || "").toLowerCase();
|
||||||
|
for (const { manifest, dormant } of this._enabledEntries()) {
|
||||||
|
if (!(manifest.siteRoutes || []).includes(h)) continue;
|
||||||
|
if (this._isFirstPartyId && !this._isFirstPartyId(manifest.id)) continue;
|
||||||
|
if (dormant) { try { await this.ensureActive(manifest.id, `${h} opened`); } catch { continue; } }
|
||||||
|
const route = this._active.get(manifest.id)?.siteRoutes.get(h);
|
||||||
|
if (route) return route;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// Enabled = running or waiting for first use.
|
// Enabled = running or waiting for first use.
|
||||||
isEnabled(id) { return this._active.has(id) || this._dormant.has(id); }
|
isEnabled(id) { return this._active.has(id) || this._dormant.has(id); }
|
||||||
|
|
||||||
|
|
@ -544,7 +577,7 @@ class AddonHost {
|
||||||
throw new Error(`main file must export an activate(api) function`);
|
throw new Error(`main file must export an activate(api) function`);
|
||||||
}
|
}
|
||||||
// Declared panels are registered up front; activate() may still add more.
|
// Declared panels are registered up front; activate() may still add more.
|
||||||
const active = { manifest, folder, exports: mod, sidebarPanels: this._resolvePanels(manifest, folder), handlers: new Map(), inject: null, tabListeners: [], ready: Promise.resolve() };
|
const active = { manifest, folder, exports: mod, sidebarPanels: this._resolvePanels(manifest, folder), handlers: new Map(), inject: null, tabListeners: [], siteRoutes: new Map(), ready: Promise.resolve() };
|
||||||
if (manifest.pageInject) {
|
if (manifest.pageInject) {
|
||||||
// Read the inject source once at activation. It's shipped to every
|
// Read the inject source once at activation. It's shipped to every
|
||||||
// matching tab's preload verbatim, so a syntax error surfaces in the
|
// matching tab's preload verbatim, so a syntax error surfaces in the
|
||||||
|
|
@ -625,6 +658,16 @@ class AddonHost {
|
||||||
// a short emoji/glyph.
|
// a short emoji/glyph.
|
||||||
// `side: "left"` puts the panel in the quick-links strip on the left
|
// `side: "left"` puts the panel in the quick-links strip on the left
|
||||||
// edge instead of the right sidebar (hosts without left panels ignore it).
|
// edge instead of the right sidebar (hosts without left panels ignore it).
|
||||||
|
// See "site-route" above. Registered per activation, so a reload or a
|
||||||
|
// disabled add-on drops it with the rest of its state.
|
||||||
|
registerSiteRoute: ({ host, handle } = {}) => {
|
||||||
|
if (!(manifest.capabilities || []).includes("site-route")) throw new Error(`registerSiteRoute requires the "site-route" capability`);
|
||||||
|
if (this._isFirstPartyId && !this._isFirstPartyId(manifest.id)) throw new Error("registerSiteRoute is reserved for built-in add-ons");
|
||||||
|
const h = String(host || "").toLowerCase();
|
||||||
|
if (!(manifest.siteRoutes || []).includes(h)) throw new Error(`registerSiteRoute: "${h}" is not in the manifest's siteRoutes`);
|
||||||
|
if (typeof handle !== "function") throw new Error("registerSiteRoute needs a handle(request) function");
|
||||||
|
active.siteRoutes.set(h, { addonId: manifest.id, handle });
|
||||||
|
},
|
||||||
registerSidebarPanel: ({ id, title, icon = manifest.icon, page, side }) => {
|
registerSidebarPanel: ({ id, title, icon = manifest.icon, page, side }) => {
|
||||||
if (!id || !title || !page) throw new Error(`registerSidebarPanel needs {id, title, page}`);
|
if (!id || !title || !page) throw new Error(`registerSidebarPanel needs {id, title, page}`);
|
||||||
const abs = path.join(folder, String(page).replace(/^[\\/]/, ""));
|
const abs = path.join(folder, String(page).replace(/^[\\/]/, ""));
|
||||||
|
|
|
||||||
14
main.js
14
main.js
|
|
@ -1973,6 +1973,20 @@ async function serveBns(request) {
|
||||||
const rawPath = url.pathname || "/";
|
const rawPath = url.pathname || "/";
|
||||||
let reqPath; try { reqPath = decodeURIComponent(rawPath); } catch { reqPath = rawPath; }
|
let reqPath; try { reqPath = decodeURIComponent(rawPath); } catch { reqPath = rawPath; }
|
||||||
|
|
||||||
|
// A built-in extension may answer paths under a name it ships with (Pithos
|
||||||
|
// serves its app at pithos.sia/<user>/<drive>/…). The root page stays the
|
||||||
|
// name's own site, and whatever the extension does not claim falls through
|
||||||
|
// to the normal lookup below.
|
||||||
|
if (rawPath !== "/" && addonHost?.siteRouteFor) {
|
||||||
|
try {
|
||||||
|
const route = await addonHost.siteRouteFor(host);
|
||||||
|
if (route) {
|
||||||
|
const out = await route.handle(request);
|
||||||
|
if (out) return out;
|
||||||
|
}
|
||||||
|
} catch (e) { console.warn(`[site-route] ${host}${rawPath}: ${e?.message || e}`); }
|
||||||
|
}
|
||||||
|
|
||||||
// (bns://collision-choose/ is handled by the will-navigate listener attached
|
// (bns://collision-choose/ is handled by the will-navigate listener attached
|
||||||
// to each tab — it fires BEFORE the request reaches this protocol handler.)
|
// to each tab — it fires BEFORE the request reaches this protocol handler.)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue