@open-ve/headers
v1.7.1
Published
Virtual-environment mechanism for Chrome MV3 extensions: per-tab request-header injection, DNR redirect/base-dir loading, build probes, a status banner, and request call-tree traces — embeddable via initVeHeaders().
Downloads
1,663
Maintainers
Readme
@open-ve/headers
An embeddable virtual-environment mechanism for Chrome MV3 extensions.
Preview a browser tab into a "virtual environment": inject a per-tab request
header (default x-virtual-env) on your API hosts, transparently load a
branch/preview build of a web app (document swap, SPA prefix, or entry-script
swap), and show a status banner with a request call-tree. It's a small,
dependency-free engine you wire into your own extension's service worker and
drive from your own UI.
Install
npm i @open-ve/headersWire it into your extension
1. Service worker — call initVeHeaders() at the top level (MV3 requires
listeners to register in the first event-loop turn):
import { initVeHeaders } from "@open-ve/headers/background";
initVeHeaders({
contentScript: "ve-content.js", // where the banner script lands in your build
defaultConfig: { // seeded on install; omit to start config-less
apiScope: ["*.example.com"], // hosts that receive the header
apps: [
{ name: "web", hosts: ["app.example.com"], docSwap: true },
],
},
});2. Manifest permissions
"permissions": ["declarativeNetRequest", "webRequest", "scripting", "storage"],
"host_permissions": ["<all_urls>"]Do not declare the banner in content_scripts — the engine registers it
dynamically from your config.
3. Banner content script — @open-ve/headers/content is a self-contained script.
Copy it into your build output at the path you passed as contentScript (e.g.
a public/ copy step: cp node_modules/@open-ve/headers/content.js public/ve-content.js).
4. UI — drive it from the typed client:
import * as ve from "@open-ve/headers/client";
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
await ve.preview(tab.id, "feature/PAY-456");
await ve.getPreview(tab.id);
await ve.exitPreview(tab.id);
await ve.setEnabled(false); // master off — exits preview every tabConfig (VEConfig)
{
headerName?: string; // default "x-virtual-env"
apiScope: string[]; // required, non-empty — host patterns that get the header
apps: Array<{
name: string;
hosts: string[];
assetsBase?: string; // default https://<first host>
docSwap?: boolean; // serve the branch document at the apex URL
kind?: "spa"; // prefix-navigate to /<identity>/…
entrySwap?: { entryFiles: string[] }; // relocate entry scripts to /<identity>/…
importMap?: string; // relocate MFE modules that have a branch build
subdomainEntry?: boolean; // <slug>.<host> → <host>?ve=<slug> (pre-DNS)
}>;
}The package is org-agnostic and ships no default config — you provide one.
Entry paths
Once configured, a tab enters a virtual environment via:
- Programmatically —
ve.preview(tabId, slug)from your UI. ?ve=<slug>on any configured host — a shareable link (non-extension viewers just get origin)./<identity>/…on a configured host that carries its own build (kind: "spa"ordocSwap) — auto-previews when a build exists at that prefix, recovering the git ref from the build's manifest (below).https://<slug>.<host>/…on hosts flagged"subdomainEntry": true— the extension rewrites the URL onto the real host before DNS, so the subdomain needs no DNS record. These URLs work only for extension users.
Build detection (ve-manifest.json)
Before it swaps anything, the engine asks: is there actually a build published
for this identity? The primary signal is a manifest at the identity root —
ve-manifest.json, written by the publisher after every asset upload, so
its presence means the build is published and complete:
{
"branch": "feature/ABC-1", // required: the git ref (the only thing an asset dir cannot yield)
"flavor": "", // "" for the bare identity, e.g. "-ci" for an ephemeral publish
"headerValue": "abc-1-1a2b", // the routing header value for this build
"identity": "feature-abc-1", // the directory this file sits in
"app": "web",
"commit": "…40-hex…",
"builtAt": "2026-09-04T12:00:00Z",
"entry": "index.html", // entry file, relative to the identity root
"publisher": "ci"
}Where it is looked for:
| App's own build (docSwap, kind:"spa") | <assetsBase>/<identity>/ve-manifest.json |
|---|---|
| an importMap module | the module's candidate URL cut right after its /<identity>/ segment (manifestUrlFor) — an MFE's identity root sits above the module file |
Only branch is required; everything else is informational. A non-JSON
content-type (an SPA host soft-404ing its own document) counts as absent.
Fallback. With no manifest, the engine falls back to the legacy probes: a
200 compared against a known-missing control path (identical length ⇒ we were
served the origin's soft-404 fallback ⇒ no build) plus, for modules, a
content-type check. These are guesses — a real build whose HTML happens to
match the fallback byte-for-byte reads as no build — so publishing a manifest
is what makes detection exact.
Which signal won is recorded per app/module in the preview record:
const { preview } = await ve.getPreview(tab.id);
preview.builds["web"];
// { source: "manifest" | "probe", url, branch, flavor, commit, builtAt,
// publisher, app, identity, entry, headerValue }source: "probe" rows carry only url (nothing else is knowable). Every probe
and manifest fetch is time-boxed at PROBE_TIMEOUT_MS = 8000 ms; a timeout
is treated exactly like a network error (not a build), so an unresponsive CDN
can never wedge a preview.
Exports
| Import | What |
|---|---|
| @open-ve/headers/background | initVeHeaders(options) — the service-worker engine |
| @open-ve/headers/content | the banner/URL-trigger content script (side-effect import / copy) |
| @open-ve/headers/client | typed wrappers over the message API, isInstalled(), and the pure helpers (fuzzyMatch/fuzzyFilter, manifestUrlFor/parseBuildManifest/summarizeBuild) |
| @open-ve/headers/lib/build-state.js | the build-state helpers on their own, for a consumer that wants them without the client |
License
MIT
