NotASithLord / NotASithLord/peerd

Firefox has no mesh host: the mesh is portable, the keyless document is what is missing

Open
#376 1 comment 0 reactions 0 assignees View on GitHub

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:

  1. Does RTCPeerConnection exist and complete a loopback offer/answer with an open data channel? transport/peer.js throws without it. A negative answer eliminates every non-document host.
  2. Bonus, and it would change the architecture: is RTCPeerConnection exposed in a Worker? If yes, the mesh can get a keyless heap on both browsers and the host question dissolves.
  3. Does crypto.subtle do Ed25519 generate/sign/verify? identity/keypair.js depends on it unconditionally and has no Firefox coverage.
  4. Does the shipped CSP permit wss:// to the signaling host? gen-manifest.ts strips the entry today.
  5. Does a self-addressed runtime.sendMessage reach the same page's own listener? This sizes the dweb-base.js refactor.

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:

  1. dweb-base.js talks to the service worker over runtime.sendMessage both directions plus a dweb-custody Port. A same-realm host needs those routes to become in-process calls behind a frozen object-identity sender, the shape direct-actor-host.js already uses. This is the largest diff and most of the risk.
  2. runtime-capabilities.js hard-wires dwebMesh to the offscreen document, so a working Firefox mesh would still report unsupported and every dweb_* tool plus a2a_run would be stripped from the model. actorIsolationCapability in peerd-runtime/actor/isolation.js is the two-host shape to copy. These must move together or a2a_run gets exposed with no host.
  3. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.