The browser Capsium reactor (@capsium/swsws): a dependency-light, hand-rolled
service worker that serves .cap packages straight from the zip.
Status: 0.2.0 — published to npm under the
@capsiumorg (see the repo-root release docs).
npm install @capsium/swsws
The published package ships the reactor building blocks as an ESM library
(dist/index.js: handleRequest, PackageStore, scope helpers, WebCrypto
providers) and the self-contained service-worker script as dist/sw.js
(IIFE, all deps bundled — register or copy it as-is).
.cap blob from the page via postMessage, verifies SHA-256
checksums against security.json (WebCrypto) and rejects tampered packages.routes.json (auto-generated routes included), unpacking entries with fflate./api/v1/introspect/
(metadata, routes, content-hashes, content-validity).security.json declares them and rejects
packages whose signature does not verify..capsium-tombstones → 404).Routes declared as { "path": "/api/v1/echo", "method": "POST", "handler": "content/handlers/echo.js" } execute in the service worker: the handler
source is read from the package, imported as an ES module through a blob:
URL, and its default export (or named fetch export) is called with the
fetch-style Request; it must produce the Response:
export default async (request) => new Response('echo');
405 Method Not Allowed (with an Allow header).Request, including its body, is passed through).502 with a clear body; a handler exception →
500..lua) are not executed: this reactor answers
501 (§4a: JS handlers execute only in JS-capable reactors).A service worker has no real sandbox: handler code runs with the
worker's full privileges — same-origin fetch, Cache API, the installed
package bytes. Only install .cap packages from sources you trust, prefer
signed packages (§6a), and deploy a Content Security Policy on the page,
e.g. script-src 'self' blob:; connect-src 'self', to constrain module
loading and exfiltration. Handler modules are imported from in-memory
blob: URLs only — never from the network.
Packages with storage.layers serve a merged view: layers are
package-relative directories mirroring the package tree, resolved top →
bottom (first hit wins); packages without a layers config behave as a
single implicit root layer. Deletions recorded in a layer's
.capsium-tombstones file (JSON array of merged-view paths) answer 404
even when a lower layer still has the file. visibility: private layers
are served to the package itself but never exposed to dependent packages.
A package with metadata.dependencies (guid → semver range) can inherit
dependency content. The browser reactor has no store directory, so the
dependency .cap blobs are supplied explicitly alongside the main
package (e.g. the page's multi-file picker posts them with the install
message); they are verified and persisted like the main package.
{ "path": "/vendor/app.js", "resource": "capsium://<guid>/content/app.js" }.
Only exported manifest resources are visible — referencing a
dependency's private resource is rejected with a clear 404.remap
(the route is served at the remapped path), responseRewrite
(body replacement, headers override), additive responseHeaders
(never override), and requestHeaders (supplanted before forwarding
to an inherited handler).authentication.json gates package routes (the introspection API stays
open):
401 + WWW-Authenticate challenge; the package's
htpasswd file is verified in pure TS. Supported hash types: bcrypt
($2a$/$2b$/$2y$) and apr1-MD5 ($apr1$). Anything else
(sha-crypt, plaintext, ...) answers 501 with a precise body, as does
a missing passwdFile.redirectPath, scopes) comes from the package; the session-cookie
signing secret comes from a deploy-time config message ({type: 'deploy-config', config: {sessionSecret}}) — never from the package.
OAuth2 without the deploy secret answers 501. Sessions are
HMAC-SHA256-signed cookies; PKCE pending state lives in memory, so a
worker restart mid-login restarts the flow.accessControl on dataset routes is enforced after
authentication (401 unauthenticated, 403 unauthorized). htpasswd
has no roles, so basic-auth principals are role-less; roles come from
the OAuth2 userinfo profile.src/sw.ts — service worker entry (built to dist/sw.js as an IIFE).src/resolver.ts — pure routes.json path+method resolution (unit-tested).src/fetch-handler.ts — request pipeline (unit-tested with a mocked store).src/handler-executor.ts — §4a ES-module handler execution.src/package-store.ts — verification + Cache-API persistence (incl.
composite dependencies).src/auth/ — §4b: htpasswd (pure-TS MD5/apr1 + bcryptjs), PKCE flow,
signed session cookies, the authentication gate.index.html — demo page with a .cap file picker.corepack yarn build # produces dist/sw.js
npx serve packages/swsws # any static server; serve the package dir
Open the page, pick a .cap (build one with @capsium/packager), then
browse /.