NotASithLord / NotASithLord/peerd
Firefox has no mesh host: the mesh is portable, the keyless document is what is missing
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 410
- Forks
- 45
- Avg merge
- 11d 5h
- Merged PRs (30d)
- 1
Description
The short version
There is no Firefox mesh host, and the mesh implementation is not the reason. peerd-distributed/ is already browser-neutral. What Chrome has and Firefox does not is a keyless document to run the mesh in.
The exclusion was never a feasibility finding. It arrived in 856caef (#359, closing #334 "Firefox advertises capabilities that require an unavailable offscreen host"), whose theme was making Firefox's capability advertising truthful. Voice was offering a card that could never work; dweb was swept into the same honesty pass. The whole gate is one predicate in packaging/gen-channel-config.ts:
dwebEnabledForTarget = (channel, browser) => channel === 'preview' && browser !== 'firefox'
That discipline was correct and must stay correct: the gates move in the same commit that makes a host real, never before.
What is already verified
The transport is portable. peerd-distributed/ reaches six globals: WebSocket, RTCPeerConnection, crypto.subtle, timers, TextEncoder/TextDecoder, btoa/atob. All are WindowOrWorkerGlobalScope members except RTCPeerConnection. Both WebSocket and RTCPeerConnection are already injectable parameters with globalThis defaults (transport/peer.js, transport/rooms.js, transport/signaling-client.js).
A sweep of the module for document|window|createElement|iframe|localStorage returns comments, the dwapp iframe transport (app hosting, not mesh, already an injectable seam in client.js), and exactly one functional hit: peerd-distributed/log.js gates a console badge on typeof window.
Nothing in the mesh persists: content store, discovery Library, DHT store, and gossip sync are in-memory Maps.
transport/ice.js already branches on Gecko's getStats shape. Someone wrote this expecting Firefox.
offscreen/dweb-base.js is itself document-free. Its only browser APIs are runtime messaging, crypto.randomUUID, and timers. Its own header says the offscreen document was chosen for lifetime, not capability: the network should outlive any single tab.
And extension/tests/dweb-twopeer.js already runs the exact production composition (joinRoom + createBaseNetwork) in an ordinary plain-HTTP page with no extension APIs at all.
Two places already claim this works and are currently false: peerd-distributed/client.js's header and docs/design/portable-identity/README.md both say Firefox loads the dweb client in its MV3 background. background/dweb-transfer.js even carries a written, commented, dead !offscreenAvailable branch. Those should be corrected either way.
The real constraint, and it is not liveness
docs/security/THREAT-MODEL.md classifies execution contexts by whether they hold the key:
| Surface | Holds the key |
|---|---|
Privileged background (background/) |
Yes. Actor loops run in separate dedicated worker heaps |
Offscreen document (offscreen/, Chrome) |
No. Actor worker heaps are keyless |
The mesh (peerd-distributed/, preview only) |
No |
The mesh is classified as a non-key-holding surface, and it gets that property from where it runs. Untrusted peer bytes are parsed directly in the host realm; unlike an actor, the mesh has no Worker heap of its own.
So the obvious port, "mirror background/direct-actor-host.js and run the mesh in the Firefox background page," does not inherit the actor precedent. That precedent holds because actor loops keep a separate Worker heap on both browsers; the key-holding realm hosts the starter, not the untrusted reasoning. Moving the mesh there puts untrusted peer bytes in the key-holding realm and makes the table above false on Firefox.
That reframes the host question. RTCPeerConnection is the one mesh dependency that is not worker-safe, so the host must be a document. On Firefox the candidate keyless documents are a dedicated extension tab (the engine-tabs/ pattern, with createTabTracker already built) or the sidebar document. The background page is the convenient answer and the architecturally wrong one.
Always-on is not required for the multi-device goal
Presence is live-only (gossip/presence.js, expiry on the order of tens of seconds) and there is no store-and-forward mailbox anywhere in the module. Both devices must be simultaneously connected regardless of host. A new install is by definition being actively used, so the only requirement is that the existing device is meshed at that moment.
Chrome's "always-on" is also weaker than the prose suggests: the mesh starts only via maybeStartBaseNetwork on unlock, resume, settings-enabled, and identity events, never at bare service-worker boot, and it cannot start while the vault is locked. An MV3 recycle drops the user's shared apps off the network until background/dweb-reseed.js re-publishes.
Conclusion: ship surface-scoped (mesh runs while a peerd surface is open). Treat always-on as a separate question, not a prerequisite.
An always-on Firefox mesh via an unbounded storage.session heartbeat was reviewed and rejected on its own merits. makeStorageSessionKeepAlive is a fail-closed detector: every tick must observe a matching onChanged inside the ack timeout or it rejects and drops the lease. Run forever, a single ack miss under memory pressure kills the mesh for the session, and both obvious repairs destroy what the acknowledgement is for. docs/security/ARC-TESTING.md also encodes the opposite discipline, and runActorLifetimeSmoke asserts the heartbeat key is absent after the last actor run.
Decision needed before any product code
Where does the Firefox mesh run, given it must be a keyless document? Dedicated extension tab (user-visible and user-closable, competes with the three engine kinds), sidebar document (per-window, needs leader election, dies on close), or accept the realm regression in the background page and update the threat model deliberately rather than silently.
First step: a capability probe, no product code
Build a scratch preview-firefox XPI using the artifact-injection pattern already in scripts/firefox/run-runtime-tests.mjs, install it over geckodriver, and answer in one session:
- Does
RTCPeerConnectionexist and complete a loopback offer/answer with an open data channel?transport/peer.jsthrows without it. A negative answer eliminates every non-document host. - Bonus, and it would change the architecture: is
RTCPeerConnectionexposed in a Worker? If yes, the mesh can get a keyless heap on both browsers and the host question dissolves. - Does
crypto.subtledo Ed25519 generate/sign/verify?identity/keypair.jsdepends on it unconditionally and has no Firefox coverage. - Does the shipped CSP permit
wss://to the signaling host?gen-manifest.tsstrips the entry today. - Does a self-addressed
runtime.sendMessagereach the same page's own listener? This sizes thedweb-base.jsrefactor.
Do not touch packaging, runtime-capabilities.js, or dweb-base.js until this is green. CI's pinned firefox-runtime job is the venue; Firefox is not installed in dev sandboxes.
Known scope if the probes pass
Three work items, all host-shaped, none protocol-shaped:
dweb-base.jstalks to the service worker overruntime.sendMessageboth directions plus adweb-custodyPort. A same-realm host needs those routes to become in-process calls behind a frozen object-identity sender, the shapedirect-actor-host.jsalready uses. This is the largest diff and most of the risk.runtime-capabilities.jshard-wiresdwebMeshto the offscreen document, so a working Firefox mesh would still report unsupported and everydweb_*tool plusa2a_runwould be stripped from the model.actorIsolationCapabilityinpeerd-runtime/actor/isolation.jsis the two-host shape to copy. These must move together ora2a_rungets exposed with no host.- Host lifecycle reconciliation that the naive plan misses entirely:
ensureDwebSuspensionRecovery,dwebCustodyReset,withDwebPublication,invalidateDwebPublications, the identity-custody-mutation deferral, and the suspension lease.
Also required: dwebEnabled defaults to { preview: true }, so flipping the packaging predicate alone turns dweb on for every Firefox preview user, which contradicts a cautious rollout. check-firefox.ts's Chrome-only allowlist is file-path bound and the refactor moves files. No packaged-page check runs under Gecko today. And the dwapp consumer surface (App engine isolation) is a separate question from the mesh host.
Store posture is unaffected: the recommended flip keeps dwebEnabledForTarget at channel === 'preview', so both store artifacts keep pruning the module, keep the inert loader stub, and keep DWEB_ENABLED = false. The only AMO exposure is the self-distributed preview XPI.
Relationship to other work
Multi-device portable identity depends on this for any Firefox participation: without a mesh host, Chrome preview to Chrome preview is the only direction that can work. #362 assumes a mesh on both ends for its gossip integration points.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the capability probe described in scripts/firefox/run-runtime-tests.mjs, using the pinned CI firefox-runtime job to test WebRTC, Ed25519, CSP, and runtime messaging. If the probes pass, read dweb-base.js, runtime-capabilities.js, direct-actor-host.js, and the listed lifecycle paths; done requires a decided keyless Firefox host and coordinated lifecycle, capability, packaging, and test changes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, typescript
- Domain
- infrastructure, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100