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)
- Chrome:
chrome://extensions, enable Developer mode, Load unpacked, pick this directory. Firefox:about:debugging#/runtime/this-firefox, Load Temporary Add-on, pickmanifest.json. A temporary Firefox add-on is removed on restart. - A disclosure page opens on first install. Read it and accept.
- On your DevPlace profile give consent to bandwidth sharing, register a device and copy the
dpn_...secret (shown once). - 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 opensWS /nodes/ws?ticket=..., then sendshellowithkind: "extension"andmodes: ["fetch"]. - A
pongis sent everyping_secondsas a keepalive and for every serverping. Achrome.alarmsalarm every minute is the reconnect backstop when the worker has been suspended. - Reconnects use full-jitter backoff between
reconnect_min_secondsandreconnect_max_seconds. Close4013retries at the minimum delay. A kick (4403), a consent or device refusal (1008with 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 inchrome.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.itand their subdomains, dot-safe suffix match). A refreshed list fromwelcomeis 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)
fetchwithcredentials: "omit",redirect: "manual",cache: "no-store", no referrer; onlyacceptandaccept-languageare 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_bytesand by the remaining daily budget, withtruncated: true;set-cookiestyle 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
welcomeframe 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.