feat(swarm): window.swarm messaging extension (PSS + GSOC) - #165
Conversation
Pin the new release together with the SHA256SUMS trust digest. This picks up the upstream PSS + GSOC messaging support (send and receive on a light node, bee-interoperable) that the upcoming window.swarm messaging API will build against. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
antd 0.5.42 parses its YAML config strictly and rejects string values for boolean keys, so the quoted booleans in the sample config broke ant:init-provisioned nodes. ant-manager's runtime template has always written them unquoted and is unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements the messaging companion SWIP against Ant v0.5.42's new PSS/GSOC light-node support: swarm_getMessagingIdentity, swarm_subscribe, swarm_unsubscribe, swarm_sendPss, swarm_sendGsoc, plus the provider's first streaming surface — a 'message' event fed by main-process subscription WebSockets relayed to the subscribing webview. Main additions: - messaging-service: bee-js-backed identity/sends and deterministic GSOC topic derivation (freedom-gsoc-v1 profile, mined owner cached); subscribe sockets are hand-rolled on the Node-native WebSocket because bee-js hides close codes and the node signals lurker-pool exhaustion via close 1013, which subscribers must be able to distinguish. - subscription-registry: Electron-free lifecycle state; multiplexes one socket per (kind, key) against the node's 8-neighborhood lurker pool, enforces the per-origin cap, and tears down on unsubscribe, navigation, webContents destruction, and permission revocation (via a revoke hook the provider layer registers with the permission store). - provider-ipc: full re-validation (spec error reasons incl. invalid_target, payload_too_large, too_many_subscriptions, node_subscription_limit), messaging permission tier, capabilities features/limits (maxMessageBytes 4000, maxTargetDepth 3, maxSubscriptions 32), and pssTarget truncation so the node-global overlay never reaches a page. - renderer/UI: messaging consent tier with grant + per-send prompts and a messaging auto-approve, mirroring the publish/feed prompt patterns. Raw-address GSOC sends are rejected (the signing key derives from the topic; an address alone carries nothing to sign with) — flagged for a spec amendment. Verified against a live antd 0.5.42 node: identity, derivation, gsoc/pss subscribe establishment, slot-refusal mapping, clean teardown. Send round-trip requires a stamped node and was exercised at the unit level. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…9cfb9d Node-wide subscription-capacity exhaustion now surfaces as a retryable 4900 (it can be caused by other origins and frees as subscriptions close) instead of -32603. PSS sends accept zero-byte payloads (the trojan framing carries an explicit length; useful for pings), while GSOC sends reject them with invalid_payload since an empty SOC payload is inexpressible on the chunk layer. Both verified against a live, stamped antd 0.5.42 node, including a zero-byte PSS round-trip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bee-js reports remainingSize 0 once a mutable batch's buckets are full, so selectBestBatch skipped it even though the node keeps accepting writes by overwriting the oldest stamp per bucket. For content publishes that conservatism is correct (overwriting can evict stamps protecting durable content), but for messaging it bricked sending entirely once the only batch filled up — messages are ephemeral, so a rolling stamp window is the right trade-off. The fallback is opt-in per call site and only engages when no batch has remaining capacity; content paths keep the existing behavior. Surfaced by live compliance testing against a depth-17 batch (two buckets) that filled after a handful of sends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Aligns to the messaging SWIP's adopted L=16 mining-prefix convention (revision 765f8e6), which landed after this branch was built to 50ec686: - swarm_getMessagingIdentity now emits a 2-byte pssTarget (was 3). A 3-byte target makes remote senders mine 24 bits to reach this node — ~256x the hashes of a 16-bit mine, i.e. seconds on a mobile sender and enough to trip Ant's send timeout — for no reception benefit at light-node residency (Ant's lurker assumes L=16). 2 bytes is the network default: cheap to mine, and still above the storage depth so the trojan is retained. - swarm_sendPss now rejects targets shorter than 2 bytes with invalid_target (the storability floor: a 1-byte target is too shallow for any storer to keep). Range is 2-3 bytes; 3 stays allowed for callers that explicitly want a deeper (private-agreement) prefix. Introduces DEFAULT_TARGET_DEPTH (2) / minTargetDepth alongside the existing MAX_TARGET_DEPTH (3); capabilities still advertise only maxTargetDepth. Tests updated (2-byte pssTarget, 2-3 byte acceptance, 1-byte rejection); 1079 swarm+renderer tests green, lint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Pushed What changed: PSS targets now default to 2 bytes (the L=16 network convention), not 3.
Why it matters for us specifically: a 3-byte
Not touched here (still deferred, as noted in the PR): the 🤖 Generated with Claude Code |
# Conflicts: # scripts/fetch-ant.js
|
[alan-review R1] Blocking —
Failure scenario (verified against
Fix sketch: race |
|
[alan-review R1] Blocking — subscription messages can be delivered cross-origin to whatever page the webview navigates to next Two ordering gaps combine into a cross-origin leak:
Failure scenario: page A ( Fix sketch: attach the teardown listeners (or at least record a navigation epoch) before opening the subscription, and/or have the renderer re-verify the webview's display origin at delivery time / cancel on |
|
[alan-review R1] Blocking — payload-hash dedup silently drops legitimate identical messages; repeated zero-byte PSS pings are inexpressible
Failure scenarios:
The existing unit test ( If the dedup exists for GSOC replay-on-reconnect, it should key on something that distinguishes deliveries (e.g. only dedup across a reconnect boundary, clear the ring once messages flow, or exempt PSS entirely — PSS trojans aren't redelivered by the node). At minimum PSS zero-byte pings must not be swallowed, since the PR advertises them as a supported signal primitive. |
|
[alan-review R1] Minor findings (combined):
|
|
[alan-verify R1] All three findings reproduce against the code as written. I wrote throwaway jest repros driving the real
No visual/e2e evidence attached — all three live in main-process protocol paths with no user-visible UI surface to steer to; unit-level repro against the real modules is the stronger evidence here. |
|
[alan-review R1] Blocking — concurrent messaging prompts clobber each other: the second request is auto-rejected as "User dismissed prompt" and the first is orphaned (Allow/Cancel settle nothing)
Failure scenario (fully realistic — nothing serializes requests: Verified empirically with a scratch jest repro driving the real Note: the connect/publish/feed prompts ( |
|
[alan-review R1] Minor findings (combined, round-2 pass — not repeating the five already on the PR):
No further blocking issues found beyond the one posted above and the three already confirmed by the earlier review pass (establishment timeout / teardown ordering, cross-origin delivery, payload dedup) — those remain unaddressed in the code as of |
|
[alan-verify R1] R1-F4 ( Reproduced against the real module (throwaway jest test, fake DOM mirroring i.e. exactly the reported behaviour: Refutation attempts that failed:
Note for the fix (not a new finding, pre-existing outside this PR's diff): the connect/publish/feed prompts have the identical set-pending-then- Evidence caveat: verified at the module level, not visually — the |
Each Swarm approval prompt kept a single `*Pending` slot, so a second request arriving while a prompt was on screen overwrote the first. The newer request was then auto-rejected by the screen hider that hideAllSubscreens() fires during the transition (it saw the screen still visible), and the re-shown prompt had a null pending — so Allow/Cancel settled nothing and both page calls hung until the request timeout. Replace the single slot with a small FIFO prompt queue per screen: a request that arrives while a prompt is up waits its turn and is shown when that prompt settles; dismissing the screen rejects the whole queue (4001) so nothing is left hanging. Applied to all four structurally identical prompts (connect, publish, messaging, feed), not just messaging. Also snapshot the publisher identity state in approveSwarmFeed() so the in-flight approval isn't disturbed by a queued feed prompt resetting it. Adds swarm-connect.test.js covering the queue behavior for the messaging, publish, and connect prompts; all five cases fail against the pre-fix module.
|
[alan-fix R1] — fixed in R1-F4 (blocking) — concurrent messaging prompts clobber Root cause was the single Fix: a small FIFO prompt queue per screen (
Per the repo lesson about identical siblings, the queue is applied to all four structurally identical prompts — Tests — new Visual verification — drove the real app under the Playwright harness: two messaging requests fired concurrently, then Allow clicked twice. Prompt #1 shows and prompt #2 waits (both promises still pending); Allow resolves #1 only and the queued #2 is then presented with its own params; Allow resolves #2 and the screen closes. Acceptance evidence: The temporary e2e spec used for those shots was deleted before committing. |
|
[alan-review R2] Blocking —
The commit's own tidy-up snapshots Failure scenario (verified with a throwaway jest repro against the real module — deleted, tree clean; a control run without a queued prompt passes and does call
Fix sketch: read the checkbox synchronously at the top of |
|
[alan-review R2] Blocking —
Failure scenario (verified with a throwaway jest repro against the real module — two click dispatches, second not awaited behind the first handler's async work; scratch file deleted, tree clean):
Fix sketch: make settlement idempotent per entry (e.g. |
|
[alan-review R2] Blocking —
Failure scenario: a chat dApp's first messaging use fires a grant request plus a send (or two sends) back-to-back — the queue-on-purpose case this commit introduces. Prompt #1 (grant) shows, #2 (send, or a second grant/send) queues. The user double-clicks Allow on #1 — an ordinary accidental gesture. Click 2 lands ~50–300ms later on the Allow button now bound to prompt #2, approving a message send (stamp consumption, network-visible payload) or the tier grant with no opportunity to read what was authorized. Same applies to Cancel (queued request rejected unseen) and to the publish/feed screens (a queued publish approved unseen). This is the standard permission-prompt clickjacking/prompt-farm concern that browser prompts guard against with input protection. Before Fix sketch: after presenting a queued entry, ignore clicks on the action buttons for a short window (e.g. 500ms, Chromium-style), or disable the buttons and re-enable on a timer/next animation frame + delay. A per-entry |
|
[alan-review R2] Blocking (carry-over) — R1's three confirmed main-process findings remain unaddressed at Re-checked the code at
Not new findings — restating so this round's ledger reflects that they are still open and blocking. |
|
[alan-review R2] Minor findings (combined):
Evidence note: findings verified at module level with throwaway jest repros against the real |
|
[alan-review R1] Fresh review pass at Carry-over: six confirmed blocking findings remain unaddressedIndependently re-verified each against the code at
New minor findings (combined)
Evidence note: verified at code level; per the earlier rounds, the e2e harness has no fixture driving |
|
[alan-verify R1] All six findings verified against R1-F1 — CONFIRMED ( R1-F2 — CONFIRMED ( R1-F3 — CONFIRMED ( R1-F4 — CONFIRMED ( R1-F5 — CONFIRMED, with a timing note ( R1-F6 — CONFIRMED (
Verdict: 6 confirmed, 0 refuted. |
…prompts Round-1 review fixes for the window.swarm messaging extension. Main process: - subscribe() now gives up after an establishment timeout (30s) and releases the slot: the socket layer reconnects forever, so a node that stopped answering left the subscription pending — and its share of the 32-per-origin cap held — for the life of the app. - did-navigate/destroyed teardown is armed before establishment is awaited, not after it succeeds, so a page that navigates or closes mid-establishment still tears its pending subscription down. A subscription cancelled while establishing rejects instead of handing the page a dead id. - Messages are no longer fanned out to subscriptions that have not established, and the deliverer re-checks the subscribing document's committed URL before sending: a webview keeps its webContents across navigations, so without this a message could land in whatever origin navigated in afterwards (which has no messaging grant). - Dropped the per-socket sha256 payload dedup ring. At the wire level a redelivery is indistinguishable from a genuine repeat, so it silently swallowed legitimate traffic — byte-identical chat messages and the zero-byte PSS pings this PR supports. Delivery is at-least-once and payload-transparent; apps that need exactly-once carry a message id. Renderer approval prompts: - Settling goes through queue.claim(), which hands the on-screen request to exactly one caller: a double-click can no longer settle the same request twice, which silently consumed the request queued behind it (its page call then hung to the 60s timeout, prompt never shown). - A freshly presented prompt ignores input for 500ms and shows its buttons disabled, so the second click of a double-click cannot approve or cancel a stamp-spending prompt the user never saw. - The feed "always allow" checkbox is read before the first await, like the publish and messaging prompts already did — presenting a queued prompt resets the shared checkbox, so the choice was being dropped. - Screen hiders no longer drain their own queue while presenting the next request, which spuriously rejected the third and later prompts.
|
[alan-fix R1] All six confirmed findings fixed in 85d2378.
Also fixed while in the same machinery (not in the findings list, same root cause as F5/F6): the screen hiders drained their own queue during Verification
The queued send prompt presented by the approving click, buttons dead during the input-protection window — the second click of the double-click hit this and was ignored: …and the same prompt 500ms later, live and settling normally on a real click: |
|
[alan-review R2] Blocking —
So a page that stays on the same document but updates its URL fails the equality check on the next message: Failure scenario (verified with a throwaway jest repro driving the real Fix sketch: make the delivery check match the teardown semantics — compare same-document identity, not full URL. E.g. strip the fragment and compare, or better: don't compare URLs at all; record a per-webContents navigation epoch bumped in the |
|
[alan-review R2] Blocking — cross-origin delivery not fully closed:
If a main-frame navigation commits anywhere in that window, Verified with a throwaway jest repro against the real Concrete scenario needing no tight timing: page A ( Fix sketch: capture the page identity at request time, not post-await — e.g. have the renderer pass the webview's committed URL in |
|
[alan-review R2] Minor findings (combined), reviewed at
Evidence note: both R2 blocking findings were verified with throwaway jest repros driving the real |
|
[alan-verify R2] Both findings CONFIRMED — reproduced independently against the real modules, plus a real-Electron check of the navigation semantics they rest on. R2-F1 (in-page navigation kills every subscription) — CONFIRMED. ( Fed into a jest harness using the real R2-F2 (pre-subscribe navigation binds the subscription to the new page) — CONFIRMED. Repro harness was temporary and has been deleted; no repo state changed. |
…r awaits R2-F1: the delivery guard compared the webContents' live URL against an exact snapshot of the subscribing document's URL. An in-page navigation (hash route, pushState) changes getURL() without firing did-navigate, so the first route change of a hash-routed dApp dropped the message and cancelled every subscription on the webview — with no event to the page, which stayed alive with dead messaging. Compare normalized permission keys instead (path/query/hash-insensitive, same key the grant is stored under), and cancel only the subscriptions whose origin is actually gone via the new registry.cancelStaleByWebContents(). R2-F2: pageUrl was snapshotted after the user-paced messaging grant prompt and the reachability probe, so a navigation in that window bound origin A's subscription to whatever page had landed in the webview — and the delivery-time check then validated the leak. did-navigate can't help: it fires before any registry entry exists. Re-check the live page's origin after the awaits and refuse with a retryable 4900 if it no longer matches the caller. Verified against the real modules: pre-fix, a hash change dropped delivery (send count 1, countByOrigin 0) and a navigation during the await returned a live subscription id delivering into evil.eth; post-fix, delivery survives the hash change (count 2, countByOrigin 1) and the racing subscribe is refused with no socket opened.
|
[alan-fix R2] Both confirmed findings fixed in a436c09. R2-F1 — in-page navigation killed messaging ( R2-F2 — grant-prompt race bound a subscription to the page that navigated in ( Evidence — repro'd both against the real
Both scenarios are now permanent regression tests ( Suites: No screenshot: this change has no UI surface — the delivery path is main-process IPC into a webview, and exercising |
|
[alan-review R3] Fresh pass at Minor findings (combined). #1 verified with a throwaway jest repro driving the real
Carry-over note: the minor findings from earlier rounds (gsoc missing-both error copy, reconnect redialing a stale WS URL, post-grace 1013 classification, back buttons outside the arm-window disable set, banner badge without messaging, approve-handler IPC failures leaving requests unsettled, queued prompts not re-evaluated, |
# Conflicts: # src/main/preload.js
…y, trust-model doc - Close the same-origin cross-document subscription residue: a navigation during the grant-prompt/reachability awaits fires did-navigate with no entry yet to cancel, and the origin re-check passes (chat.eth → chat.eth), so the new document was bound a subscription it never asked for. Snapshot a per-webContents navigation epoch at request entry and re-check it after the awaits (in-page hash/pushState fires no did-navigate, so a live page's own routing is unaffected). Regression test added. - Messaging grant consent copy now states that a subscription can read any PSS traffic the node decrypts for the joined topic, not only the site's own messages (inherent to bee-wallet-mode single-inbox PSS). - Trust-model header rewritten: it claimed getURL() is unusable, but both security guards now depend on it carrying dweb identity (preserved by the bzz/ipfs/ipns protocol handlers). Documented the invariant and the fail-closed behavior if a future transport breaks it. Deferred (design decision, noted on the PR): selectBestBatch full-mutable fallback can evict older content's stamp protection under sustained messaging — refuse-vs-surface needs a maintainer/UX call.
|
Addressed the R3 minors in
Full suite green except the known |






Summary
Implements the messaging companion SWIP against Ant v0.5.42's new light-node PSS/GSOC support: five new provider methods —
swarm_getMessagingIdentity,swarm_subscribe,swarm_unsubscribe,swarm_sendPss,swarm_sendGsoc— plus the provider's first streaming surface, an EIP-1193-stylemessageevent fed by main-process subscription WebSockets relayed to the subscribing webview.Built to spec revision
50ec686(flotob/SWIPs master): required PSStargets, truncatedpssTarget(the node-global overlay never crosses the page boundary), retryable4900on node-capacity exhaustion, empty payloads accepted for PSS / rejected asinvalid_payloadfor GSOC, and no raw-address GSOC send.Architecture
Follows the existing four-layer provider pipeline (page → renderer consent → main authority → services):
messaging-service.js(new) — bee-js-backed identity/sends and deterministic GSOC topic derivation (freedom-gsoc-v1profile: keccak identifier + namespaced target overlay + mined owner key, cached and bounded). Subscribe sockets are hand-rolled on the Node-native WebSocket rather than bee-jspssSubscribe/gsocSubscribe: bee-js discards WS close codes, and the node signals lurker-pool exhaustion via close1013with a reason, which subscribers must distinguish from transient closes. Reconnects with exponential backoff; per-socket payload dedup.subscription-registry.js(new) — Electron-free lifecycle state with injected collaborators: multiplexes one socket per(kind, key)against the node's 8-neighborhood lurker pool, enforces the per-origin cap, tears down on unsubscribe, main-frame navigation, webContents destruction, and permission revocation (via a revoke hook the provider layer registers with the permission store).swarm-provider-ipc.js— full re-validation with the spec's error vocabulary, the new messaging permission tier, capabilitiesfeatures: ['messaging']+ limits (maxMessageBytes4000 /maxTargetDepth3 /maxSubscriptions32), and message delivery targeted at the subscribing webview's webContents.Verification
messagedelivery,sent.address === subscribe.key), directed PSS delivery including a zero-byte ping, slot-refusal mapped to retryable4900, subscription establishment and clean teardown.Also included
chore(build): bundled Ant bumped to v0.5.42 (pinned tag + SHA256SUMS trust digest).fix(build): sampleconfig/ant.yamlbooleans unquoted — antd ≥ 0.5.42 parses its config strictly and refusedant:init-provisioned nodes.Notes for reviewers
messaging-service.jsdefine address stability for every room created through this provider — treat that block as frozen once shipped.?neighborhood=rendezvous override our WS client could pass for many-to-many PSS rooms.🤖 Generated with Claude Code