mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
54 lines
3.2 KiB
JavaScript
54 lines
3.2 KiB
JavaScript
'use strict';
|
|
|
|
// The widget iframe's origin, and what it costs.
|
|
//
|
|
// The player mounts a widget in an iframe sandboxed to `allow-scripts` with NO allow-same-origin,
|
|
// which gives it an OPAQUE origin: widget scripts cannot read the player's window, its localStorage,
|
|
// or its device token. That is deliberate and worth keeping — widget HTML is operator-authored and
|
|
//, for the webpage widget, third-party.
|
|
//
|
|
// It has a consequence that is easy to forget and was in fact forgotten: a service worker does not
|
|
// control opaque-origin clients, so the widget frame's navigation NEVER reaches sw.js. The
|
|
// cache-first widget branch there is real code that this player's own widgets do not use — measured
|
|
// in Chrome, a clock widget mounted five times over 25 seconds while the shell cache held zero
|
|
// widget entries, and a plain fetch() of the same URL from the controlled page was cached
|
|
// immediately. Widgets survive an outage today on the HTTP cache and the server's immutable
|
|
// Cache-Control, not on the worker.
|
|
//
|
|
// So this test pins the security property, and pins the fact that the offline story for widgets
|
|
// rests on the HTTP header. Someone who "fixes" offline widgets by adding allow-same-origin trades
|
|
// the isolation for a cache — the wrong direction, and the reason this is asserted rather than left
|
|
// as a comment.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const HTML = fs.readFileSync(path.join(__dirname, '..', 'player', 'index.html'), 'utf8');
|
|
const SW = fs.readFileSync(path.join(__dirname, '..', 'player', 'sw.js'), 'utf8');
|
|
const WIDGETS = fs.readFileSync(path.join(__dirname, '..', 'routes', 'widgets.js'), 'utf8');
|
|
|
|
test('the widget iframe stays null-origin by default, with explicit per-item opt-in only', () => {
|
|
assert.match(HTML, /function widgetSandboxAttr\(item\)/, 'widget sandbox policy should be centralized');
|
|
assert.match(HTML, /item && item\.widget_allow_same_origin/, 'opt-in must be keyed by item/org setting');
|
|
assert.match(HTML, /allow-scripts allow-same-origin/, 'explicit opt-in token must exist');
|
|
assert.match(HTML, /: 'allow-scripts'/, 'safe default remains allow-scripts only');
|
|
});
|
|
|
|
test('the offline guarantee for widgets is the HTTP header, and the server still sets it', () => {
|
|
// If this regresses to no-store, widgets stop surviving an outage everywhere — and the service
|
|
// worker will NOT quietly cover for it, because it never sees the request.
|
|
assert.match(WIDGETS, /max-age=31536000, immutable/,
|
|
'a rev-pinned render must stay hard-cacheable: it is the only thing holding widgets offline');
|
|
assert.match(WIDGETS, /no-store/, 'a render with no rev must stay uncacheable — nothing distinguishes one from the next');
|
|
});
|
|
|
|
test('the worker does not claim to be what keeps widgets offline', () => {
|
|
// The comment above that branch used to say it was. A worker cannot control an opaque-origin
|
|
// client, so the claim was false in exactly the deployment it was written for.
|
|
const branch = SW.slice(0, SW.indexOf("url.pathname.startsWith('/api/widgets/')"));
|
|
assert.match(branch, /opaque-origin|opaque origin/i,
|
|
'sw.js must record that the widget frame is opaque-origin and bypasses it');
|
|
});
|