v0.0.31 - Advanced JS tests: ES Modules, Web Workers, Shared Workers, WebAssembly, cross-origin fetch; Service Worker analysis plan

This commit is contained in:
Laan Tungir
2026-07-16 11:35:57 -04:00
parent ae102323f6
commit 6020df2e92
12 changed files with 524 additions and 4 deletions
+13 -1
View File
@@ -326,6 +326,16 @@ block local-file web apps.
- **Form submissions** — GET forms append query strings to the target
page; POST forms navigate (body silently dropped, no server)
- **`window.open()`** — opens local pages in new tabs
- **ES Modules** — dynamic `import()` works with local module files
- **Web Workers and Shared Workers** — `new Worker()` and
`new SharedWorker()` with local scripts enable background computation
threads (including cross-tab shared workers)
- **WebAssembly** — `WebAssembly.instantiate()` works for compiled code
- **Cross-origin fetch to remote HTTPS** — `fetch('https://example.com')`
from a `file://` page works with no CORS blocking (confirms the
"deprecated web security" design goal)
- **Cross-origin remote resources** — remote `<script src>`,
`<link rel=stylesheet>`, and `<img>` from HTTPS CDNs all load correctly
**Known limitations (WebKit engine quirks, not bugs):**
@@ -335,8 +345,10 @@ block local-file web apps.
(valid media works perfectly)
- Cache API (CacheStorage) rejects non-HTTP/HTTPS URLs — use IndexedDB
instead for local-file storage
- Service Workers don't work on `file://` (W3C spec requires HTTP/HTTPS) —
use Web Workers for background processing instead
**Test site:** A comprehensive multipage test website with 45+ edge cases
**Test site:** A comprehensive multipage test website with 53 edge cases
is in [`tests/local-site/`](tests/local-site/index.html). See
[`tests/local-site/LOCAL-FILE-BROWSING-REPORT.md`](tests/local-site/LOCAL-FILE-BROWSING-REPORT.md)
for the full test report.
+1 -1
View File
@@ -1 +1 @@
0.0.30
0.0.31
+124
View File
@@ -0,0 +1,124 @@
# Service Workers on file:// — Analysis & Options
## The Problem
`navigator.serviceWorker.register()` on a `file://` page rejects with:
> "serviceWorker.register() must be called with a script URL whose protocol is either HTTP or HTTPS"
## Where the Check Lives
The error message is generated **inside WebKit's web process** (the
JavaScript engine / WebCore layer), not in the WebKitGTK C API layer.
The check is in WebKit's `ServiceWorkerContainer::register()` C++ code
(in `Source/WebCore/workers/service/ServiceWorkerContainer.cpp`), which
validates the script URL protocol against a hard-coded list of
`http` and `https`.
This is **not** controllable via:
- `WebKitSettings` (no setting for SW protocol allowlist)
- `WebKitSecurityManager` (only controls local/secure/cors/empty/no-access
scheme classification — not SW registration eligibility)
- `WebKitWebsiteDataManager` (only controls the storage *directory* for SW
registrations, not the protocol gate)
## Options
### Option A: Patch WebKit (fork / rebuild) — HIGH EFFORT
We could fork `webkitgtk` (or maintain a patch set) that removes or
relaxes the protocol check in `ServiceWorkerContainer.cpp`. This is the
only way to make `navigator.serviceWorker.register()` accept `file://`
URLs natively.
**Pros:**
- Fully native — JS code works unchanged
- SW lifecycle (install/activate/fetch events) works as designed
**Cons:**
- Requires maintaining a WebKit fork or patch set against Debian's
`webkit2gtk` package
- Every WebKitGTK update requires re-applying the patch
- Build complexity — WebKit is ~35MB of system lib; rebuilding from
source is heavy
- The SW spec assumes HTTP semantics (scope, update checks, fetch
interception) that don't map cleanly to `file://`
**Verdict:** Too heavy for the benefit. Service Workers on `file://` is a
niche use case.
### Option B: Register file:// as a "secure" scheme — ALREADY DONE, DOESN'T HELP
We already call `webkit_security_manager_register_uri_scheme_as_secure()`
for `file` in `src/main.c:850`. This makes `file://` origins treated as
"potentially trustworthy" for some purposes (like `navigator.geolocation`
or `getUserMedia`), but **does not** bypass the SW registration protocol
check — that check is a separate hard-coded `http/https` test in
WebCore.
### Option C: Use a custom URI scheme instead of file:// — MODERATE EFFORT
Instead of loading local sites via `file:///path/to/index.html`, we could
register a custom scheme (e.g. `local://`) via
`webkit_web_context_register_uri_scheme()` that serves files from disk.
If we also register `local://` as secure via `WebKitSecurityManager`, SW
registration *might* work — but only if WebKit's SW code checks
"potentially trustworthy origin" rather than hard-coding `http/https`.
This needs testing.
**Pros:**
- No WebKit fork needed
- We control the scheme handler entirely
- Could also solve the Cache API issue (Issue #5)
**Cons:**
- Changes the URL model — users see `local://` instead of `file://`
- Relative path resolution may differ
- Still might not work if WebKit's SW check is truly hard-coded to
`http/https` (need to test)
- Significant implementation effort (scheme handler, path mapping,
security registration)
**Verdict:** Worth investigating if SW on local files becomes important.
The first step would be a quick test: register `local://` as secure,
load a page, try `navigator.serviceWorker.register()`.
### Option D: JavaScript shim / polyfill — LOW EFFORT, PARTIAL
We could inject a JS shim that overrides `navigator.serviceWorker.register`
to do something useful on `file://` — e.g., load the SW script as a Web
Worker instead, or implement a minimal fetch interceptor.
**Pros:**
- No WebKit changes
- We already inject JS shims (`window.nostr`)
**Cons:**
- Not a real Service Worker — no `fetch` event interception, no
`install`/`activate` lifecycle, no `clients` API
- Would only provide a subset of SW functionality
- Complex to implement convincingly
**Verdict:** Only useful if someone specifically needs SW-like behavior
on `file://` and is willing to accept a polyfill.
### Option E: Do nothing — RECOMMENDED
Service Workers are designed for HTTP/HTTPS origins. On `file://`:
- **Web Workers** work perfectly (tested ✅) — use those for background
computation
- **Shared Workers** also work perfectly (tested ✅) — use those for
cross-tab/cross-page shared background computation
- **IndexedDB** works perfectly (tested ✅) — use that for offline
storage
- **Cache API** doesn't work (Issue #5) — but local files are already
local, so caching is redundant
- **fetch interception** (the main SW use case) is less relevant when
everything is local
The "deprecated web security" design goal is about **cross-origin
access** (CORS/SOP), which works. Service Workers are a different
concern — they're about **offline web apps and PWA features**, which
are inherently HTTP-oriented concepts.
**Verdict:** Document the limitation and move on. If a specific use case
emerges that needs SW on `file://`, revisit Option C (custom scheme).
+2 -2
View File
@@ -11,9 +11,9 @@
#ifndef SOVEREIGN_BROWSER_VERSION_H
#define SOVEREIGN_BROWSER_VERSION_H
#define SB_VERSION "v0.0.30"
#define SB_VERSION "v0.0.31"
#define SB_VERSION_MAJOR 0
#define SB_VERSION_MINOR 0
#define SB_VERSION_PATCH 30
#define SB_VERSION_PATCH 31
#endif /* SOVEREIGN_BROWSER_VERSION_H */
@@ -75,6 +75,15 @@ The issues below are polish/UX improvements.
| 43 | `<object>` / `<embed>` with SVG | [`media.html`](media.html) | ✅ PASS | `data`/`src` resolved to file:// URL |
| 44 | Inline SVG | [`media.html`](media.html) | ✅ PASS | |
| 45 | Clicking a link to navigate | [`index.html`](index.html) → [`about.html`](about.html) | ✅ PASS | `click` MCP tool triggered navigation |
| 46 | ES Modules (`import()`) | [`advanced.html`](advanced.html) | ✅ PASS | Dynamic `import()` works; exports accessible (greet, add, PI) |
| 47 | Web Workers | [`advanced.html`](advanced.html) | ✅ PASS | `new Worker()` created; `postMessage` round-trip works (7*6=42) |
| 47b | Shared Workers | [`advanced.html`](advanced.html) | ✅ PASS | `new SharedWorker()` created; `port.onmessage` works; ping/pong round-trip works |
| 48 | Service Workers | [`advanced.html`](advanced.html) | ❌ FAIL | `register()` rejects: "must be called with HTTP or HTTPS". See Issue #6. |
| 49 | WebAssembly | [`advanced.html`](advanced.html) | ✅ PASS | `WebAssembly.instantiate()` works; `add(3,4)=7`, `add(100,200)=300` |
| 50 | Cross-origin fetch (file:// → https://) | [`advanced.html`](advanced.html) | ✅ PASS | `fetch('https://example.com')` returns status=200, type=basic, 559 chars. **No CORS blocking!** |
| 51 | Cross-origin remote image | [`advanced.html`](advanced.html) | ✅ PASS | Remote images from placehold.co and google.com load correctly |
| 52 | Cross-origin remote script | [`advanced.html`](advanced.html) | ✅ PASS | Remote `<script src>` from jsdelivr CDN loaded and executed |
| 53 | Cross-origin remote stylesheet | [`advanced.html`](advanced.html) | ✅ PASS | Remote `<link rel=stylesheet>` from jsdelivr CDN loaded and applied |
---
@@ -206,6 +215,31 @@ recommended alternative and works fully.
---
### Issue #6 — Service Workers don't work on file:// (MINOR / WebKit restriction)
**What happens:** `navigator.serviceWorker.register()` rejects with the
error: "serviceWorker.register() must be called with a script URL whose
protocol is either HTTP or HTTPS".
**Why it matters:** Service Workers cannot be used on `file://` pages.
This means PWA features like offline caching via service workers,
background sync, and push notifications won't work for local-file apps.
**Likely cause:** This is a W3C specification restriction — Service
Workers are only allowed on HTTP/HTTPS origins. WebKit enforces this at
the `register()` level. This is consistent with all major browsers.
**Suggested fix:** None required — this is a fundamental specification
restriction. Web Workers (regular, non-service workers) DO work on
`file://` and can be used for background computation. For offline
storage, use IndexedDB.
**Severity:** Low. Service Workers are a niche use case for local-file
apps. Web Workers are the alternative for background processing and work
fully.
---
## What Works Surprisingly Well
These are the things that could have been broken but weren't:
@@ -248,6 +282,26 @@ These are the things that could have been broken but weren't:
The `<audio>` element similarly works with correct duration. Media
playback is fully functional for local files.
10. **ES Modules** — dynamic `import()` works on `file://` pages, allowing
modern modular JavaScript with local module files.
11. **Web Workers and Shared Workers**`new Worker()` and
`new SharedWorker()` with local script files both work, enabling
background computation threads and cross-tab shared workers from
`file://` pages.
12. **WebAssembly**`WebAssembly.instantiate()` works, enabling
high-performance compiled code on `file://` pages.
13. **Cross-origin fetch to remote HTTPS URLs**`fetch('https://example.com')`
from a `file://` page returns `status=200, type=basic` with no CORS
blocking. This confirms the "deprecated web security" design goal is
working — local web apps can freely call remote APIs.
14. **Cross-origin remote resources** — remote `<script src>`, `<link
rel=stylesheet>`, and `<img>` from HTTPS CDNs all load and execute
correctly from `file://` pages.
---
## Recommended Actions
@@ -327,6 +381,7 @@ tests/local-site/
├── query.html # Query string handling
├── encoded name.html # URL-encoded filename (literal space)
├── 404.html # Placeholder (not actually missing)
├── advanced.html # Advanced JS test (ES Modules, Workers, WASM, cross-origin fetch)
├── assets/
│ ├── site.css # Shared stylesheet
│ ├── favicon.svg # SVG favicon
@@ -335,6 +390,11 @@ tests/local-site/
│ ├── data.json # JSON for fetch/XHR tests
│ ├── home.js # External script for index.html
│ ├── external.js # External script for scripts/external.html
│ ├── advanced-runner.js # Advanced JS test runner
│ ├── es-module-test.js # ES Module (import/export) test
│ ├── worker-test.js # Web Worker script
│ ├── shared-worker-test.js # Shared Worker script
│ ├── sw-test.js # Service Worker script (expected to fail on file://)
│ ├── sample.mp4 # Test video (Big Buck Bunny, gitignored)
│ └── Neon Dream.m4a # Test audio (gitignored)
├── scripts/
+19
View File
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Local Site — Advanced JS Test</title>
<link rel="stylesheet" href="assets/site.css">
</head>
<body>
<header>
<h1>Advanced JS Test Page</h1>
<nav><a href="index.html">Home</a></nav>
</header>
<main>
<h2>Results</h2>
<pre id="results">Running tests...</pre>
</main>
<script src="assets/advanced-runner.js"></script>
</body>
</html>
+247
View File
@@ -0,0 +1,247 @@
// Advanced JS test runner — writes results to #results as tests complete.
(function () {
var log = [];
function flush() {
var el = document.getElementById('results');
if (el) el.textContent = log.join('\n');
}
function add(msg) {
log.push(msg);
flush();
}
add('=== Advanced JS Test Start ===');
add('URL: ' + location.href);
add('');
// ── 1. ES Modules ──────────────────────────────────────────────
add('--- ES Modules ---');
add('Testing <script type="module"> with local import...');
// We'll use a dynamic import() to test module loading
try {
import('./es-module-test.js')
.then(function (mod) {
add('import(): ok');
add(' mod.greet("World") = ' + mod.greet('World'));
add(' mod.add(2, 3) = ' + mod.add(2, 3));
add(' mod.PI = ' + mod.PI);
add('ES MODULES: PASS');
testWebWorker();
})
.catch(function (e) {
add('import(): ERROR - ' + e.message);
add('ES MODULES: FAIL');
testWebWorker();
});
} catch (e) {
add('import() not supported: ' + e.message);
add('ES MODULES: FAIL (import() not supported)');
testWebWorker();
}
// ── 2. Web Workers ─────────────────────────────────────────────
function testWebWorker() {
add('');
add('--- Web Workers ---');
add('Testing new Worker() with local script...');
try {
var worker = new Worker('assets/worker-test.js');
add('new Worker(): ok (created)');
worker.onmessage = function (e) {
add('worker.onmessage: ' + e.data);
add('WEB WORKERS: PASS');
worker.terminate();
testServiceWorker();
};
worker.onerror = function (e) {
add('worker.onerror: ' + (e.message || 'error'));
add('WEB WORKERS: FAIL');
testServiceWorker();
};
// Send a message to the worker
worker.postMessage({ cmd: 'compute', a: 7, b: 6 });
add('postMessage sent: {cmd: "compute", a: 7, b: 6}');
} catch (e) {
add('new Worker(): ERROR - ' + e.message);
add('WEB WORKERS: FAIL');
testServiceWorker();
}
}
// ── 3. Service Workers ─────────────────────────────────────────
function testServiceWorker() {
add('');
add('--- Service Workers ---');
add('available: ' + ('serviceWorker' in navigator));
if (!('serviceWorker' in navigator)) {
add('SERVICE WORKERS: not available');
testWebAssembly();
return;
}
try {
navigator.serviceWorker.register('assets/sw-test.js')
.then(function (reg) {
add('register(): ok (scope=' + reg.scope + ')');
add('SERVICE WORKERS: PASS (registered)');
testWebAssembly();
})
.catch(function (e) {
add('register(): ERROR - ' + (e.message || e));
add('SERVICE WORKERS: FAIL');
testWebAssembly();
});
} catch (e) {
add('register(): EXCEPTION - ' + e.message);
add('SERVICE WORKERS: FAIL');
testWebAssembly();
}
}
// ── 4. WebAssembly ─────────────────────────────────────────────
function testWebAssembly() {
add('');
add('--- WebAssembly ---');
add('available: ' + (typeof WebAssembly !== 'undefined'));
if (typeof WebAssembly === 'undefined') {
add('WEBASSEMBLY: not available');
testCrossOriginFetch();
return;
}
try {
// Minimal Wasm module: exports an "add" function
// (wasm binary: (module (func (export "add") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)))
var bytes = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f,
0x03, 0x02, 0x01, 0x00,
0x07, 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00,
0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b
]);
WebAssembly.instantiate(bytes).then(function (result) {
var addFn = result.instance.exports.add;
add('instantiate(): ok');
add(' add(3, 4) = ' + addFn(3, 4));
add(' add(100, 200) = ' + addFn(100, 200));
add('WEBASSEMBLY: PASS');
testCrossOriginFetch();
}).catch(function (e) {
add('instantiate(): ERROR - ' + e.message);
add('WEBASSEMBLY: FAIL');
testCrossOriginFetch();
});
} catch (e) {
add('instantiate(): EXCEPTION - ' + e.message);
add('WEBASSEMBLY: FAIL');
testCrossOriginFetch();
}
}
// ── 5. Cross-origin fetch to remote HTTP/HTTPS ─────────────────
function testCrossOriginFetch() {
add('');
add('--- Cross-origin fetch (file:// → https://) ---');
// Test 1: fetch a remote page
add('Test 1: fetch("https://example.com")...');
try {
fetch('https://example.com')
.then(function (r) {
add(' status=' + r.status + ' ok=' + r.ok + ' type=' + r.type);
return r.text();
})
.then(function (text) {
add(' body length=' + text.length + ' chars');
add(' body starts with: ' + text.substring(0, 80).replace(/\n/g, ' '));
add('CROSS-ORIGIN FETCH (page): PASS');
testRemoteImage();
})
.catch(function (e) {
add(' ERROR - ' + e.message);
add('CROSS-ORIGIN FETCH (page): FAIL');
testRemoteImage();
});
} catch (e) {
add(' EXCEPTION - ' + e.message);
add('CROSS-ORIGIN FETCH (page): FAIL');
testRemoteImage();
}
}
// Test 2: load a remote image
function testRemoteImage() {
add('');
add('Test 2: remote image from https://...');
var img = new Image();
img.onload = function () {
add(' loaded: ' + img.naturalWidth + 'x' + img.naturalHeight);
add('CROSS-ORIGIN IMAGE: PASS');
testRemoteScript();
};
img.onerror = function () {
add(' ERROR: image failed to load');
add('CROSS-ORIGIN IMAGE: FAIL');
testRemoteScript();
};
img.src = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8a/Banana-Single.jpg/240px-Banana-Single.jpg';
add(' img.src set to remote URL...');
}
// Test 3: load a remote script
function testRemoteScript() {
add('');
add('Test 3: remote <script src> from https://...');
var s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js';
s.onload = function () {
add(' loaded: script executed');
add(' typeof confetti = ' + (typeof confetti));
add('CROSS-ORIGIN SCRIPT: PASS');
testRemoteStylesheet();
};
s.onerror = function () {
add(' ERROR: script failed to load');
add('CROSS-ORIGIN SCRIPT: FAIL');
testRemoteStylesheet();
};
document.head.appendChild(s);
add(' script.src set to remote URL...');
}
// Test 4: load a remote stylesheet
function testRemoteStylesheet() {
add('');
add('Test 4: remote <link rel=stylesheet> from https://...');
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdn.jsdelivr.net/npm/water.css@2/out/light.css';
link.onload = function () {
add(' loaded: stylesheet applied');
add(' stylesheet count = ' + document.styleSheets.length);
add('CROSS-ORIGIN STYLESHEET: PASS');
finishAll();
};
link.onerror = function () {
add(' ERROR: stylesheet failed to load');
add('CROSS-ORIGIN STYLESHEET: FAIL');
finishAll();
};
document.head.appendChild(link);
add(' link.href set to remote URL...');
}
function finishAll() {
add('');
add('=== Advanced JS Test Complete ===');
flush();
}
})();
+16
View File
@@ -0,0 +1,16 @@
// ES Module test — imported by advanced-runner.js via dynamic import()
export const PI = 3.14159;
export function greet(name) {
return 'Hello, ' + name + '!';
}
export function add(a, b) {
return a + b;
}
export default {
PI: PI,
greet: greet,
add: add
};
@@ -0,0 +1,18 @@
// Shared Worker test — can be shared across multiple tabs/pages
var connections = 0;
self.onconnect = function (e) {
var port = e.ports[0];
connections++;
port.postMessage('SharedWorker connected! Total connections: ' + connections);
port.onmessage = function (ev) {
var msg = ev.data;
if (msg.cmd === 'ping') {
port.postMessage('pong from SharedWorker (connections=' + connections + ')');
} else if (msg.cmd === 'count') {
port.postMessage('SharedWorker has ' + connections + ' active connection(s)');
} else {
port.postMessage('SharedWorker received: ' + JSON.stringify(msg));
}
};
};
+12
View File
@@ -0,0 +1,12 @@
// Service Worker test — minimal SW that just responds to install/activate
self.addEventListener('install', function (event) {
self.skipWaiting();
});
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim());
});
self.addEventListener('fetch', function (event) {
// Pass-through — don't intercept anything
});
+10
View File
@@ -0,0 +1,10 @@
// Web Worker test — receives a message, computes, posts result back
self.onmessage = function (e) {
var msg = e.data;
if (msg.cmd === 'compute') {
var result = msg.a * msg.b;
self.postMessage('Worker computed ' + msg.a + ' * ' + msg.b + ' = ' + result);
} else {
self.postMessage('Worker received unknown command: ' + msg.cmd);
}
};
+2
View File
@@ -62,6 +62,8 @@
<li><a href="query.html?foo=bar&baz=qux">Query-string page</a></li>
<li><a href="hash.html#section-2">Hash-fragment page</a></li>
<li><a href="scripts/external.html">External script page</a></li>
<li><a href="advanced.html">Advanced JS page (ES Modules, Workers, WASM, cross-origin fetch)</a></li>
<li><a href="storage-test.html">Automated storage test (all types)</a></li>
</ul>
<h2>Inline image test</h2>