A Scramjet plugin that emulates the chrome.* extension APIs inside proxied frames, so
Chrome extensions (Vencord-style userscripts, ad blockers, etc.) can run against sites
loaded through Scramjet. Originally forked from
carbonicality/amethyst; this version is a
full TypeScript rewrite built directly around Scramjet's plugin API instead of a
standalone postMessage-bridged iframe runtime, and ships no UI of its own — it's a
library, the host app builds whatever extension-manager UI it wants on top.
Sapphire(src/sapphire.ts) — orchestrator owning installed-extension state (IndexedDB-backed), background contexts, and content-script registrations. Create one per app.SapphireContentScriptPlugin(src/SapphirePlugin.ts) — a ScramjetManagedPlugin. Create one perFrame(matching Scramjet's own convention, see@mercuryworkshop/scramjet-controller'screateFrame({ plugins: [...] })), passing it the sharedSapphireinstance and that frame's tab id.chromeApi.ts— builds a livechrome.*/browser.*object directly on the target realm's realWindow(obtained from Scramjet'sframe.hooks.init.precontext). No postMessage bridge:addListenercaptures real function references from that realm, so dispatch is a direct call, not a serialized round-trip. This fixes real bugs the old amethyst design had (e.g.chrome.runtime.getManifest()returningundefinedbecause it was silently async under the hood) and removes a class of injection-timing races.- Background pages/scripts/service-workers all run in one persistent, invisible iframe
per extension (not tied to any tab's lifecycle) — a deliberate simplification over
spec-faithful Worker-based MV3 semantics; see the doc comment in
background.ts. dnr.tsexports a purecheckDeclarativeNetRequest()matcher; it is not wired into Scramjet's fetch path (that hook is undocumented/unstable — see project notes), so wire it into your own request routing if you want extension-driven blocking.
import { Sapphire, SapphireContentScriptPlugin } from "@x8r/sapphire";
const sapphire = new Sapphire({
host: {
getTabId: (win) => /* map a proxied window back to your tab id */,
getTab: (id) => /* { id, windowId, url, title, active } */,
getAllTabs: () => /* TabInfo[] */,
getTabWindow: (id) => /* live Window for tabs.executeScript/cookies.* */,
navigateTab: (id, url) => /* your router */,
},
});
await sapphire.init(); // loads previously-installed extensions
// per tab, when creating its Scramjet frame:
controller.createFrame(iframeEl, {
plugins: [new SapphireContentScriptPlugin(sapphire, tabId)],
});
// installing an extension (host owns the file picker / drag-drop UI):
const extId = await sapphire.installExtension(await file.arrayBuffer(), file.name);