Files
hub/tools/screentime-test.js
T
nmemmertandClaude Sonnet 5.5 74592c25a5
build-container / build (push) Successful in 5m57s
iPad and Screen Time helper; v2.0 roadmap
- Admin screen: the simple Screen Time setup, the web addresses the app needs, addresses devices were seen reaching, and a Check this device test
- Report-only Content-Security-Policy so devices report any other address a page loads (nothing is blocked)
- Pictures from other sites pass through the hub (/api/img): podcast search covers, Plex Home pictures, a VBV cover, search answers
- docs/v2-roadmap.md: the family-first 2.0 plan, with Family Chat (text only) and no photos or calls

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 11:07:05 -04:00

25 lines
3.2 KiB
JavaScript

// Screen Time helper: the report-only policy, what devices report reaching, the admin overview, and the picture pass-through refusing what it should.
const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path');
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-st-')); process.env.ONLYOFFICE_URL = 'https://docs.example.org';
const users = require('../users'), st = require('../screentime');
const people = { a: users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), b: users.create({ name: 'Bob', role: 'user', handle: 'Yg' }) };
const rep = (uri, dir = 'img-src') => JSON.stringify({ 'csp-report': { 'blocked-uri': uri, 'effective-directive': dir, 'violated-directive': dir + " 'self'" } });
const hub = http.createServer((req, res) => { const me = req.headers['x-user'] === 'b' ? people.b : people.a; users.runAs(me, () => st.handle(req, res, me)); }).listen(0, async () => {
const port = hub.address().port, B = 'http://127.0.0.1:' + port, call = (p, o = {}) => fetch(B + p, { ...o, headers: { 'x-forwarded-host': 'hub.example.org', ...(o.headers || {}) } });
try {
const pol = st.policy(); assert.match(pol, /report-uri \/api\/csp-report/); assert.match(pol, /frame-src[^;]*https:\/\/docs\.example\.org/, 'the document editor is allowed'); assert.match(pol, /img-src 'self' data: blob:;/, 'pictures only from the hub');
for (const [u, d] of [['https://is1-ssl.mzstatic.com/a.jpg', 'img-src'], ['https://is1-ssl.mzstatic.com/b.jpg', 'img-src'], ['https://api.open-meteo.com/v1/forecast?x=1', 'connect-src'], ['inline', 'script-src'], ['data', 'img-src'], ['https://hub.example.org/x.png', 'img-src'], ['http://localhost:3000/x', 'img-src']]) assert.equal((await call('/api/csp-report', { method: 'POST', body: rep(u, d) })).status, 204);
assert.equal((await call('/api/csp-report', { method: 'POST', body: 'not json' })).status, 204, 'a garbled report is ignored, not an error');
let o = await (await call('/api/screentime')).json();
assert.equal(o.required[0].host, 'hub.example.org'); assert.deepEqual(o.required.map(x => x.host), ['hub.example.org', 'docs.example.org'], 'the hub and the editor');
assert.deepEqual(o.seen.map(x => [x.host, x.n]), [['is1-ssl.mzstatic.com', 2], ['api.open-meteo.com', 1]], 'outside addresses are counted; the hub itself, local addresses and non-addresses are left out'); assert.deepEqual(o.seen[1].what, ['connect']);
assert.equal((await call('/api/screentime', { headers: { 'x-user': 'b' } })).status, 403, 'only an admin sees the list');
o = await (await call('/api/screentime', { method: 'DELETE' })).json(); assert.deepEqual(o.seen, []);
// pictures from other sites
assert.equal((await call('/api/img?u=notaurl')).status, 400); assert.equal((await call('/api/img?u=' + encodeURIComponent('http://example.com/a.png'))).status, 502, 'only https');
assert.equal((await call('/api/img?u=' + encodeURIComponent('https://127.0.0.1/a.png'))).status, 502, 'never a private address'); assert.equal((await call('/api/img?u=' + encodeURIComponent('https://localhost/a.png'))).status, 502);
console.log('screentime ok');
} catch (e) { console.error(e); process.exitCode = 1; }
hub.close(); setTimeout(() => process.exit(process.exitCode || 0), 50);
});