openpencil/packages/op-chrome-extension/wasm-core.js
Kayshen-X 64d70d672d feat(extension): add OpenPencil web-capture Chrome extension
MV3 extension that captures the rendered active tab (via the shared
snapshot-extractor contract) and imports it into OpenPencil. Logic lives
in the new op-chrome-extension-core crate (wasm): endpoint rules,
chunked-transfer integrity, /mcp envelope + reply classification,
download-name sanitisation, SSO/account session parsing, and hub
snapshot-inbox delivery. JS is glue only (chrome.* APIs, fetch, popup
DOM, injected page functions).

Capture: full page + element pick. Delivery: local ingress
(POST /api/import/web-snapshot) with /mcp fallback, JSON download, and —
when signed in to OP Hub — the account snapshot inbox. Flat popup UI,
15-locale strings with an in-popup language switcher, store packaging.

Why a crate: keeps the security-sensitive logic in tested Rust rather
than glue JS, and the SW/popup split keeps dynamic import() out of the
service-worker graph (guarded by check-sw-imports).
2026-08-04 21:43:01 +08:00

70 lines
2.8 KiB
JavaScript

/**
* The **popup's** loader for the Rust logic core.
*
* `wasm/` is a build product and is not checked in, so a fresh clone has no
* core until `scripts/build-wasm.sh` has run. The import is therefore dynamic
* and guarded: a static `import` of a missing module aborts the whole popup
* script before any handler is attached, which would present as a blank popup
* with no explanation. A dynamic one lets the popup come up and say what to
* run.
*
* **This file is for the popup only.** `import()` is disallowed on
* `ServiceWorkerGlobalScope` by the HTML specification (w3c/ServiceWorker
* issue 1356), so the service worker must not reach the core through here —
* it imports the shim with a static declaration instead. See
* `core-registry.js` for the split, and `scripts/check-sw-imports.mjs` for the
* guard that keeps this file out of the worker's import graph.
*
* The shim is loaded from the extension package by its own origin, so MV3's
* `script-src 'self'` covers it. Instantiating the module needs the
* `'wasm-unsafe-eval'` CSP keyword, which `manifest.json` declares — that
* keyword permits `WebAssembly.compile` on bundled bytes; it does not permit
* `eval`, and no code is fetched from anywhere but this package.
*/
import { coreError, getCore, hasCore, setCore } from './core-registry.js';
/** Generated by `wasm-bindgen --target web`; named after the crate's lib name. */
const CORE_MODULE = './wasm/op_chrome_extension_core.js';
/** In-flight load, so concurrent callers share one instantiation. */
let loading = null;
/**
* Load and instantiate the core. Idempotent; the result is cached.
*
* @returns {Promise<object>} the wasm module's exports.
* @throws {Error} with `code` `wasmMissing` when the build product is absent,
* or `wasmInit` when it is present but will not instantiate.
*/
export function loadCore() {
if (hasCore()) return Promise.resolve(getCore());
if (!loading) {
loading = (async () => {
let module;
try {
module = await import(CORE_MODULE);
} catch (cause) {
// The import is the only step that can tell "not built" apart from
// "built but broken". Everything after it is an init failure, and
// saying "run the build script" about one of those is a misdiagnosis.
throw coreError('wasmMissing', cause);
}
try {
// `--target web` exports its initializer as the default binding; with
// no argument it resolves the `.wasm` sitting next to the shim.
await module.default();
} catch (cause) {
throw coreError('wasmInit', cause);
}
return setCore(module);
})().catch((cause) => {
// Drop the rejected promise so a later attempt can retry rather than
// replaying the same failure forever.
loading = null;
throw cause;
});
}
return loading;
}