Files
devplacepy/nodes/extension/README.md
T
retoor 6f80a820da
DevPlace CI / test (push) Failing after 3m13s
Zo doen wijdat in oost
2026-10-16 07:20:44 +02:00

6.0 KiB

DevPlace Node, browser extension

A Manifest V3 extension for Chrome and Firefox from one source tree. It lets a DevPlace member donate a little of their connection to the community node network, in fetch mode only (an extension has no raw sockets). The protocol is devplacepy/services/nodes/spec.py, served at GET /nodes/spec.json; this extension never restates a number from it except in src/spec.js, which a test compares against spec.py.

Install (sideload)

  1. Chrome: chrome://extensions, enable Developer mode, Load unpacked, pick this directory. Firefox: about:debugging#/runtime/this-firefox, Load Temporary Add-on, pick manifest.json. A temporary Firefox add-on is removed on restart.
  2. A disclosure page opens on first install. Read it and accept.
  3. On your DevPlace profile give consent to bandwidth sharing, register a device and copy the dpn_... secret (shown once).
  4. Open the popup, enter the server URL and the secret, grant the host permission for that server when asked, then switch sharing on.

Packaged build: ./build.sh <outdir> writes devplace-node-extension.zip and devplace-node-extension.zip.sha256. The zip holds manifest.json, src/, ui/ and this file; tests are not shipped.

Chrome and Firefox from one tree

manifest.json declares both background.service_worker and background.scripts, both pointing at the same ES module.

Key Used by
background.service_worker Chrome (service worker). Firefox ignores it with a console warning.
background.scripts Firefox (event page). Chrome ignores it when a service worker is present.
browser_specific_settings.gecko Firefox only (add-on id, minimum 128). Chrome warns about the unknown key and ignores it.

Both use the chrome.* namespace with promises. Firefox store submission additionally needs a data_collection_permissions declaration, not added yet.

How it works

  • The background worker holds the WebSocket. It exchanges the device secret for a 30 second ticket (POST /nodes/ticket, Authorization: Bearer dpn_...) and opens WS /nodes/ws?ticket=..., then sends hello with kind: "extension" and modes: ["fetch"].
  • A pong is sent every ping_seconds as a keepalive and for every server ping. A chrome.alarms alarm every minute is the reconnect backstop when the worker has been suspended.
  • Reconnects use full-jitter backoff between reconnect_min_seconds and reconnect_max_seconds. Close 4013 retries at the minimum delay. A kick (4403), a consent or device refusal (1008 with a reason) or a rejected secret (HTTP 401/403) halts and shows the reason; nothing hammers the server.
  • State (connection, last error) is in chrome.storage.session. Config (server URL, secret, limits, disclosure flag) and the usage counters are in chrome.storage.local.
  • The secret is never logged and never shown after saving; the popup shows its first 8 characters only.

Safety enforced in the extension, before every request

Independent of anything the server says:

  • allowlist: the hosts baked into src/spec.js (reddit.com, redd.it and their subdomains, dot-safe suffix match). A refreshed list from welcome is accepted only entry by entry when it is equal or narrower (same or sub-suffix, methods and ports subsets, fetch mode present); a widening entry is dropped and reported in the popup
  • https only, port 443 only (the spec's 80 is never used because plain http is refused), no credentials in the URL, method must be permitted (GET)
  • fetch with credentials: "omit", redirect: "manual", cache: "no-store", no referrer; only accept and accept-language are forwarded from the job, so no cookie or authorization header can be injected
  • each redirect hop is re-checked against the allowlist, at most 5 hops; a redirect that leaves the allowlist is refused with redirect_not_allowed
  • response body capped at max_body_bytes and by the remaining daily budget, with truncated: true; set-cookie style response headers are dropped
  • timeout by AbortController, daily bandwidth cap, concurrency cap and active hours from the local settings, each answered with its refusal code (timeout, bandwidth_exhausted, concurrency_exceeded, outside_hours). A server value can only lower these, never raise them

Known limit: browsers hide the target of a redirect: "manual" response (an opaque redirect), so the extension cannot read where it points. It fails closed with redirect_not_allowed. A followable redirect is only handled when the response exposes its Location.

Permissions

Permission Why
storage config, usage counters, connection state
alarms reconnect backstop while the worker is suspended
host_permissions: reddit.com, *.reddit.com, redd.it, *.redd.it over https fetch the allowlisted destinations without CORS. These are exactly the destinations in spec.py.
optional_host_permissions: any https origin, localhost, 127.0.0.1 over http requested at connect time for the one server origin you enter, so the ticket request works. Not granted at install.

No tabs, cookies, history, webRequest or <all_urls> permission.

What is and is not shared

Shared: the URL of the requested public page, the page content that comes back, your public IP address as seen by the destination, and request and byte counts. Not used: your cookies, logins, browsing history, tabs, other sites, any host outside the list, any non-https or non-443 destination. See ui/disclosure.html, shown on first install.

Tests

node --test nodes/extension/test/*.mjs (no dependencies). They cover Policy, Limits, Fetcher with a stubbed fetch, WsClient with a fake WebSocket, and a drift check of src/spec.js against spec.py. Not covered by automated tests: background.js and the popup, which need a real browser.

Gaps

  • No icons yet; the browser shows a default.
  • The welcome frame carries no device label, so the label in the popup is local only.
  • Not tested in a real Chrome or Firefox session in this change.