Skip to content

Clients load no code from third-party hosts at runtime - #3041

Merged
colinmegill merged 1 commit into
edgefrom
self-contained-clients
Oct 12, 2026
Merged

colinmegill merged 1 commit into
edgefrom
self-contained-clients

Conversation

@colinmegill

Copy link
Copy Markdown
Member

Observable changes

  • No runtime requests to third-party hosts from the report, admin or legacy participation pages, other than the deliberate ones listed below. Gone: d3js.org, cdn.plot.ly, maxcdn.bootstrapcdn.com, fonts.googleapis.com / fonts.gstatic.com, cdnjs.cloudflare.com (Twemoji images), cdn.jsdelivr.net. The same files now come from our own host.
  • Pages look the same. A Delphi-backed report rendered on this branch with every non-local host blocked, against edge with internet: topicReport screenshots are byte-identical; report differs in 752 of 39.3M pixels and topicStats in 157 pixels, all in the page-header URL text (the local server port) and the export file names (the timestamp). Every chart is pixel-identical.
  • Bundle sizes.
    • Report: report_bundle.*.js unchanged byte for byte (639,317 bytes, same content hash). index_report.html 1,085 → 977 bytes. New static files, byte-identical to what the CDNs served: d3 4.13.0 (221,957), d3-scale-chromatic 1.5.0 (19,831), Plotly 2.27.0 (3,598,158), each with a .gz sibling like the bundle has. The Font Awesome 4.4.0 stylesheet (26,711) is no longer loaded: nothing in the report uses its classes.
    • Admin: admin_bundle.*.js 248,663 → 248,740 bytes gzipped. New static files: Space Mono CSS (4,140), woff2 for latin, latin-ext and vietnamese in 400/700/400-italic (124,672), woff fallbacks (93,280), 15 Twemoji PNGs (11,949).
    • Legacy participation: bundles unchanged; js/jwt-decode-3.1.2.js (3,688 bytes, 1,365 gzipped) served.
  • New static files have versioned paths and a one-year cache header, like the bundles.

No server, database, schema or route change. The participation path is untouched: the only participation-client change is where jwt_decode is loaded from.

What changed

client was now
report d3js.org/d3.v4.min.js (4.13.0), d3js.org/d3-scale-chromatic.v1.min.js (1.5.0), cdn.plot.ly/plotly-2.27.0.min.js as window.d3 / window.Plotly the same files from pinned npm packages (d3@4.13.0, d3-scale-chromatic@1.5.0 as aliases so the bundled d3 4.6 and d3-scale-chromatic 1.1 modules stay as they are; plotly.js-dist-min@2.27.0), copied unminified into the build and loaded by the same script tags in the same order
report Font Awesome 4.4.0 CSS from maxcdn removed (unused)
admin Space Mono from Google Fonts @fontsource/space-mono@5.3.0: same font version (1.003), all 225 latin glyph outlines and advance widths identical (checked with fontTools); same three styles, subsets and font-display: swap
admin react-easy-emoji images from cdnjs (Twemoji 14.0.2) the 15 images the pages use, byte-identical, shipped under public/twemoji/14.0.2/72x72, via a one-line wrapper that sets the base URL (credits Twemoji's CC-BY 4.0 in source)
legacy participation cdn.jsdelivr.net/npm/jwt-decode@latest/... (resolved to 3.1.2) jwt-decode@3.1.2 build/jwt-decode.js, same synchronous position

Deliberate third parties, unchanged, for a ruling

  • Simple Analytics (scripts.simpleanalyticscdn.com, queue.simpleanalyticscdn.com): report, admin (production builds), legacy participation.
  • Google Analytics (www.googletagmanager.com): legacy participation, only when GA_TRACKING_ID is set.
  • Login provider: the OIDC issuer (Auth0 in production). The test OIDC simulator's sign-in page itself loads unpkg.com/tailwindcss and cdn.auth0.com/js/auth0/9.19.0.
  • Embeds: /donate (donorbox.org iframe); /home2 (topic-map demo iframe from the polis-delphi S3 bucket); /embed, /embedReport and their preprod variants (hardcoded pol.is / preprod.pol.is).
  • Delphi interactive visualizations (datamapplot HTML from S3, framed by commentsReport and topicsVizReport): the generated HTML loads unpkg.com (deck.gl@latest, apache-arrow@latest), Google Fonts and maxcdn. Fixing that is a change to how Delphi generates them, not to the clients.

The guard

e2e/cypress/e2e/self-contained/third-party-hosts.cy.js (npm run test:self-contained; also part of npm test): blocks every non-local request, clears and disables the browser cache (cached files never reach the network, so without this the check passed on edge once another page had loaded them), and loads every report view the server routes (including the topic and collective statement views), both participation clients, the signed-out admin pages and the signed-in conversation pages. It fails on any blocked request to a host other than Simple Analytics, any page error, or any of our own scripts, styles, fonts or images failing to load, and checks window.d3.version, window.Plotly.version, window.jwt_decode, the Space Mono faces and the emoji images. The report browser proof (ci/dynamo-removal/report-proof.cjs) now blocks non-local hosts the same way, so a CDN outage can no longer fail it. Each client has a unit test pinning its template and versions.

Found along the way (not changed)

  • The alpha participation page reports a React hydration mismatch (minified error Show permission error for admin UI when not moderator #418) on first load, on edge and on this branch alike. The guard allows exactly that error on that page and logs it.
  • e2e/cypress/support/e2e.js swallows every uncaught exception, so a broken page never failed the suite; the guard records exceptions itself.
  • client-participation npm run lint reports 43 errors on edge and on this branch, none in changed files; no CI job runs it.

Checks run (local Docker stack, non-default ports)

  • Guard on this branch: 17 of 17 pass. Guard on edge: 0 of 17. Each report view lists d3js.org (twice), cdn.plot.ly and maxcdn; legacy participation lists cdn.jsdelivr.net; admin pages list fonts.googleapis.com and cdnjs.cloudflare.com. The alpha page loads nothing third-party itself; its test fails on edge only through the admin page its sign-out step visits.
  • bash scripts/test-dynamo-removal.sh on this branch: pass (report, topicStats and topicReport rendered with non-local hosts blocked; only Simple Analytics was blocked; 0 page errors). The same report proof on edge with hosts blocked fails: Cannot read properties of undefined (reading 'scaleLinear') plus the four CDN requests.
  • client-report: npm install, npm test (18 suites, 94 tests), npm run lint, npm run build:prod.
  • client-admin: npm ci, npm run test:coverage (29 suites, 324 tests), npm run lint, npm run build:prod.
  • client-participation: npm ci, npm test (159 tests), npm run build:prod.
  • e2e: npm run lint (0 errors). Existing specs client-report/*, client-admin/public-pages, client-admin/conversation, client-participation/anonymous-jwt-flow and alpha-client/votes pass; client-report/comment-report fails only on its hardcoded http://localhost/report/ (the local run used 127.0.0.1 on another port).
  • Lockfiles: additions only; npm install leaves them unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

The report, admin and legacy participation pages fetched code, styles,
fonts and images from CDNs on every load. When one of those hosts was
unreachable the page broke (the report reads window.d3 from d3js.org),
and every load told those hosts who was viewing.

Each dependency now comes from a pinned npm package and is served from
the client's own build, at the version the page was already loading:

- report: d3 4.13.0, d3-scale-chromatic 1.5.0 and Plotly 2.27.0 are
  copied byte for byte into the build and loaded by the same script tags,
  in the same order, from our host; the unused Font Awesome 4.4.0
  stylesheet (no element uses its classes) is removed.
- admin: the Space Mono font comes from @fontsource/space-mono 5.3.0
  (the same font version and glyphs as Google Fonts serves); the fifteen
  Twemoji 14.0.2 images the pages draw are shipped as client assets.
- legacy participation: window.jwt_decode is jwt-decode 3.1.2, the
  version the jsdelivr @latest link resolved to.

Simple Analytics, Google Analytics (only when configured), the login
provider and the deliberate embeds are unchanged.

A Cypress check blocks every non-local host, clears and disables the
browser cache, and loads every report view (including topic and
collective statement views), both participation clients and the admin
pages; it fails on any third-party request other than Simple Analytics,
any page error, or any of our own assets failing to load. The report
browser proof blocks non-local hosts the same way. Unit tests in each
client pin the templates and versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
@colinmegill
colinmegill merged commit 8a6ed09 into edge Oct 12, 2026
14 checks passed
@colinmegill
colinmegill deleted the self-contained-clients branch October 12, 2026 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant