Skip to content

SWIP-XXXX: Swarm Provider API (window.swarm)#94

Open
flotob wants to merge 10 commits into
ethersphere:masterfrom
flotob:master
Open

SWIP-XXXX: Swarm Provider API (window.swarm)#94
flotob wants to merge 10 commits into
ethersphere:masterfrom
flotob:master

Conversation

@flotob

@flotob flotob commented May 9, 2026

Copy link
Copy Markdown

Summary

This PR proposes a new draft SWIP defining a standard JavaScript provider API — window.swarm — that lets web pages request access to a user's Swarm (Bee) node for publishing data, uploading files, managing mutable feeds, reading and writing indexed feed entries, and introspecting their own feed records. It follows the request/response pattern established by EIP-1193 for Ethereum providers, adapted for Swarm's publishing and feed primitives.

The draft lives at SWIPs/swip-draft_provider_api.md and will be renamed to swip-XXXX.md once a number is assigned.

Motivation

Today, publishing to Swarm from a web app requires either:

  1. Direct HTTP calls to a locally running Bee node — exposing the full API surface (including administrative, debug and staking endpoints) to any page that can reach localhost, or
  2. Server-side publishing infrastructure — defeating the point of decentralized hosting.

Neither is acceptable for a user-sovereign web. Users need a way to grant web apps controlled, permissioned access to Swarm publishing — the way window.ethereum lets pages interact with a wallet without exposing private keys. Without a standard, every Swarm-enabled browser or extension will invent its own API, fragmenting the ecosystem.

What the SWIP specifies

  • Provider object (window.swarm) injected before DOMContentLoaded, with request(), convenience wrappers, and an event interface.
  • Ten RPC methods covering: connection (swarm_requestAccess), capability discovery (swarm_getCapabilities), publishing (swarm_publishData, swarm_publishFiles, swarm_getUploadStatus), mutable feeds (swarm_createFeed, swarm_updateFeed), indexed feed entries (swarm_writeFeedEntry, swarm_readFeedEntry), and origin-scoped feed introspection (swarm_listFeeds).
  • Permission model with explicit user consent, origin normalization (so all dweb content served from 127.0.0.1 doesn't collapse into one permission scope), and app-scoped feed identities (each origin's feeds signed by a per-origin derived key, so dApps can't impersonate the user's main wallet).
  • Read-only methods on public Swarm data (feed entry reads, listing the calling origin's own feeds) require no permission grant, since the same data is fetchable via any Bee gateway and gating it would be friction without security benefit.
  • Exact-match requirements on indexed feed reads/writes (with implementation notes on Bee's GET /feeds/{owner}/{topic} at-or-before semantics and the SOC-address fallback) so overwrite protection and explicit-index reads are safe.
  • Capability-advertised limits (maxDataBytes, maxPathBytes) so future implementations supporting PAX tar can advertise larger paths without breaking the spec.
  • Security considerations covering origin trust, resource exhaustion, iframe behavior, key material isolation, and temp artifact cleanup.
  • Future extensions explicitly out of scope for 1.0 (capabilitiesChanged event, preferredIdentityMode, encoding parameter for reads, promotion path for specVersion).

Reference implementations

This is not a paper spec — both sides of the API are already implemented and exercise the full surface:

  • Provider: Freedom Browser (PR #19) injects window.swarm into webview contexts, with main-process IPC enforcement, origin normalization, app-scoped feed keys, and per-origin permissions.
  • Consumer dApp: Swarmit — a decentralized message board — uses swarm_requestAccess, swarm_publishData, swarm_createFeed, swarm_writeFeedEntry, swarm_readFeedEntry, and swarm_listFeeds for its full publishing and profile-discovery pipeline.

The implementation work surfaced several spec issues that have already been folded back into this draft (USTAR tar's 100-byte path limit, Bee's at-or-before feed lookup semantics, the SOC-address endianness trap, permission gating for public reads).

Status

  • Status: Draft — opening this PR to register the SWIP and begin discussion.
  • Developer Documentation: Swarm Provider JavaScript API
  • Discussion: Swarm Discord — SWIPs channel.
  • Happy to iterate on naming, scope, and any specifics before this is endorsed. Filename will be renamed from swip-draft_provider_api.md to swip-XXXX.md once a number is assigned.

flobot and others added 6 commits April 3, 2026 20:12
…provements

Add Security Considerations section (origin trust, resource exhaustion, iframe
behavior, feed key material, temp artifacts). Add Future Extensions section
(swarm_listFeeds, capabilitiesChanged, preferredIdentityMode, specVersion
promotion path). Promote convenience methods and re-prompting from SHOULD to
MUST. Add specVersion (SHOULD) to swarm_getCapabilities. Document UTF-8 string
encoding, session-scoped tag ownership, feed update failure semantics, and
bzz:// URI scheme status. Add edge-case test cases. Fix stale branch links.
Rename file to match repo conventions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eedEntry

Two coordinated updates reflecting the implemented behavior in Freedom
Browser (PR ethersphere#19):

1. New method: swarm_listFeeds()
   - Origin-scoped introspection returning the calling origin's previously
     created feed records — name, topic, owner, manifestReference, bzzUrl,
     createdAt, lastUpdated, lastReference.
   - No permission required: feed coordinates are deterministic given
     (origin, name) and the metadata persists across permission revocation
     by design, so gating it would be friction without security benefit.
   - Returns [] for origins with no feeds (never an error).
   - Removed from the Future Extensions list since it now ships in 1.0.

2. Permission correction for swarm_readFeedEntry
   - The 1.0 draft previously required connection permission. That was
     wrong — feeds are public Swarm data readable from any Bee gateway
     without auth, and forcing requestAccess broke passive use cases like
     profile pages displaying on-chain-discovered activity feeds.
   - Method now requires NO permission grant. Updated description, errors
     (4100 removed), behavior bullets, permission lifecycle item 4, and
     the corresponding test case ("Without Feed Grant" → "Without Any
     Permission" with stronger assertions).
   - Permission lifecycle item 5 (disconnection) clarified that
     permission-free reads MUST continue to function after revocation.

Other touches:
- Method count in the abstract bumped from "nine" to "ten".
- Convenience method list extended with window.swarm.listFeeds().
- Implementation reference updated: removed the broken docs/swarm-provider-test
  link (the file moved to research/ and is no longer tracked); Swarmit's
  method usage list extended to include writeFeedEntry / readFeedEntry /
  listFeeds.

Net diff: +83 / -14 lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reflects implementation findings from the iOS Freedom Browser provider:

- USTAR tar's 100-byte name field caps file paths far below the prior
  256-char wording. Replace the hardcoded limit with a capability-advertised
  maxPathBytes (normative floor of 100, recommended default 100 UTF-8 bytes)
  so future PAX-supporting implementations can advertise more.
- Bee's GET /feeds/{owner}/{topic}?index=N is at-or-before, not exact-match,
  which silently breaks writeFeedEntry's overwrite protection on sparse
  writes and corrupts readFeedEntry's explicit-index contract. Add normative
  exact-match requirements on both methods plus an informative section
  documenting the SOC-address derivation (GET /chunks/{socAddress}) and the
  big-endian/little-endian endianness trap.
- Clarify that the 4 KB SOC body limit is internal to the storage envelope,
  not a writeFeedEntry payload cap; the dApp-visible cap is maxDataBytes.
@agazso

agazso commented May 19, 2026

Copy link
Copy Markdown

It would be good if chunk level operations were supported in the API, because with those every higher level Swarm APIs can be implemented (feeds, manifest, ACT etc.), even ones that are not supported at the moment by Bee (e.g. epoch feeds).

The API could be window.swarm.publishChunk({ chunkPayload, span, options }) where chunkPayload is the chunk payload with type Uint8Array | string, and an optional span that can be omitted and it defaults to the payload length (in bytes). options would be also optional, and it can contain other fields controlling advanced functionality (pin, encryption, tag, deferred, redundancy).

Similarly there would be a window.swarm.readChunk({ reference, options }) which would return the chunk data. The reference could be either Uint8Array | string and it could contain a regular or an encrypted reference. The options would be optional and it can contain other fields controlling advanced functionality (redundancy, ACT).

There are also Single-Owner Chunks. Currently Swarm does not have a way to distinguish different chunk types, and it does it with heuristics, but there are documented issues and discrepancies about it.
See:

If this could be fixed at the Swarm level, then publishChunk would be enough for publishing and only a readSingleOwnerChunk would be needed additionally, otherwise also a publishSingleOwnerChunk would be necessary. But maybe that's out of the scope of this SWIP already.

…ission model

In response to PR review (agazso, ethersphere#94):

- New low-level chunk methods: swarm_publishChunk, swarm_readChunk,
  swarm_writeSingleOwnerChunk, swarm_readSingleOwnerChunk. Type is
  declared by method name to sidestep Bee's CAC/SOC heuristic
  (SWIP-67, bee#5445). Reads MUST validate by recomputing the
  type-specific address; mismatch returns chunk_type_mismatch.
- New swarm_getSigningIdentity for pre-write SOC address derivation;
  also serves as feed-permission bootstrap path.
- Permission model: prompt-rejection is 4001 across both publish and
  feed tiers; 4100 reserved for missing connection or un-promptable
  contexts. 4100 description broadened to match.
- Permission-free reads (readFeedEntry, readChunk, readSingleOwnerChunk,
  listFeeds) now MUST be per-origin rate/bandwidth limited; new
  rate_limited reason and dedicated security section.
- span widened to number | bigint for u64 safety.
- Capability advertises maxChunkPayloadBytes = 4096.
- Design Goal scoped to writes/signing/resource-consuming ops to match
  the permission-free read tier.
- Implementation section honestly scopes Freedom/Swarmit coverage to
  the original high-level surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@flotob

flotob commented May 19, 2026

Copy link
Copy Markdown
Author

Thanks @agazso — this ended up being very useful feedback. The latest push (d42af03) takes the chunk tier substantively on board.

Chunk methods added: swarm_publishChunk / swarm_readChunk for CACs, and swarm_writeSingleOwnerChunk / swarm_readSingleOwnerChunk for SOCs. The chunk type is declared by the method name rather than inferred, which avoids depending on the SOC/CAC heuristic ambiguity discussed in #67 and ethersphere/bee#5445.

Type validation on reads: providers must verify returned bytes against the requested chunk type: BMT/address validation for CACs, and SOC signature recovery plus keccak256(identifier || owner) derivation for SOCs. A mismatch returns chunk_type_mismatch.

Signing identity discovery: added swarm_getSigningIdentity, so dApps can pre-compute SOC addresses, e.g. for epoch-feed construction, before committing to a specific write. It also gives a clean bootstrap path for the feed/signing permission grant.

options exists, but is intentionally narrow in v1: chunk methods accept an options object, but no fields are standardized yet. Unknown fields must return unsupported_option. ACT, encryption, redundancy, progress tags, cache behavior, and deferred upload are listed as future-extension candidates pending a precise mapping to Bee’s endpoint semantics.

SOC mutability wording corrected: same (owner, identifier) rewrites are undefined/discouraged; mutable structures should use distinct identifiers, with swarm_writeFeedEntry retaining overwrite-rejection semantics for the journal/feed use case.

A few related changes fell out of this: permission-free reads now have mandatory per-origin rate/bandwidth limits with a rate_limited reason; span is now number | bigint for u64-safe custom BMT spans; and 4001 vs 4100 is now consistent across publish/feed permission prompts.

I also looked at SwarmID while shaping this. My current read is that window.swarm should provide the node-access, permission, origin, postage-selection, and signing boundary, while libraries such as SwarmID can build identity/encryption/ACT/higher-level structures on top of the chunk substrate. Happy to coordinate on what a SwarmID adapter would need from this interface.

Reference implementations for the new chunk tier and swarm_getSigningIdentity will follow in a subsequent Freedom Browser PR; the existing high-level methods are already implemented there. Feedback on the new surface before that work starts is very welcome.

@flotob

flotob commented May 23, 2026

Copy link
Copy Markdown
Author

Hi @agazso, quick follow-up: we implemented the chunk-level provider API discussed here in Freedom Browser.

Implementation PR: solardev-xyz/freedom-browser#81

It includes:

  • CAC methods: swarm_publishChunk / swarm_readChunk
  • SOC methods: swarm_writeSingleOwnerChunk / swarm_readSingleOwnerChunk
  • swarm_getSigningIdentity
  • permission-free read budgets for public reads
  • explicit CAC/SOC type validation with chunk_type_mismatch
  • structured provider error reasons

We also smoke-tested the CAC and SOC paths against Bee. The smoke test caught an SOC upload endpoint issue, which is now fixed: SOC writes use Bee's /soc/{owner}/{identifier} API and reads work both by SOC address and by owner+identifier.

Thanks for the idea that led to this amendment; it translated cleanly into the provider implementation.

@flotob

flotob commented May 27, 2026

Copy link
Copy Markdown
Author

Quick additional follow-up: we also built a first experimental consumer library on top of the chunk-level API:

https://github.com/solardev-xyz/swarm-kit

The interesting part is that this validates the original reason for adding raw CAC/SOC access: higher-level Swarm data structures can now be implemented outside the provider/Bee API surface.

swarm-kit currently includes helpers/primitives for:

  • CAC/SOC byte, text, and JSON helpers
  • deterministic SOC identifier derivation
  • object/chunk-graph storage above one raw chunk
  • epoch feeds
  • indexed SOC streams
  • hash chains
  • multi-writer feeds
  • DID-style / revisioned owner records
  • encrypted object helpers
  • signed document records
  • commit/reveal pairs
  • provider compliance checks for window.swarm

The playground and compliance harness have been smoke-tested in Freedom Browser against the implemented window.swarm chunk methods.

This is still experimental, but it is a useful data point for the SWIP: the chunk tier does seem to move many future Swarm patterns from “new provider/Bee method needed” into “library-level convention.”

flotob added a commit to solardev-xyz/freedom-browser-ios that referenced this pull request Jul 12, 2026
Brings the mobile window.swarm provider up to the current SWIP draft
(ethersphere/SWIPs#94), matching the desktop implementation
(solardev-xyz/freedom-browser#81). The mobile side had only the
original ten high-level methods; dapps using the newer surface failed
with "Method not supported: swarm_getSigningIdentity".

New methods:
- swarm_publishChunk / swarm_readChunk (CAC tier, publish permission /
  permission-free)
- swarm_writeSingleOwnerChunk / swarm_readSingleOwnerChunk (SOC tier,
  feed-permission signing / permission-free)
- swarm_getSigningIdentity (feed-permission tier; no-prompt fast path
  once granted, sheet-bootstrapped first grant)

Spec-mandated behaviors that came with the update:
- chunk-type validation on reads (BMT recompute for CACs, signature
  recovery + address re-derivation for SOCs) → chunk_type_mismatch
- per-origin rate/bandwidth budgets on the four permission-free read
  methods (600 req / 5 MB connected, 120 req / 512 KB anonymous, 60 s
  windows) → rate_limited
- structured reasons: invalid_reference, invalid_identifier,
  invalid_span, chunk_not_found, chunk_type_mismatch,
  unsupported_option, rate_limited
- span as number | bigint across the JS bridge (bigint → decimal
  string → u64; read replies convert back)
- capabilities: maxChunkPayloadBytes (4096), publisherIdentityModes,
  extensions.publisherSigning
- owner fields normalized to EIP-55 checksummed 0x form across
  createFeed / listFeeds / writeSingleOwnerChunk / getSigningIdentity
  (spec requires one identical owner string; matches desktop)

Verification: 821 unit tests green, including a new end-to-end test
that runs swarm-kit's runSwarmProviderCompliance harness inside a real
WKWebView against the real preload + bridge + vault signing (12/12
compliance cases pass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds swip-draft_provider_api_messaging.md — a requires-extension to the
provider API adding real-time messaging: swarm_subscribe/unsubscribe (the
first streaming surface, EIP-1193 'message'-event style), swarm_sendPss,
swarm_sendGsoc, and swarm_getMessagingIdentity. New messaging permission
tier, feature detection via getCapabilities().features, best-effort/
unordered/at-least-once delivery semantics.
@flotob

flotob commented Jul 14, 2026

Copy link
Copy Markdown
Author

I've pushed a companion draft to this branch — swip-draft_provider_api_messaging.md — proposing a messaging extension (PSS + GSOC) to the provider API, and wanted to surface it here for feedback since it builds directly on this spec.

Why an extension: the v1 surface is entirely request/response — there's no way for a dApp to receive asynchronous data (the only push signals are connect/disconnect). That's fine for publishing and mutable feeds, but it structurally can't support real-time applications (collaborative editing, chat, presence), which need a channel over which the node delivers messages as they arrive. Polling feeds gets you async sync but never interactive latency, and it can't express PSS's push-only encrypted point-to-point delivery at all.

What it adds (four methods + one event):

  • swarm_subscribe / swarm_unsubscribe — the first streaming surface, using the EIP-1193 eth_subscribe pattern (subscription id + a message event) to stay idiomatic.
  • swarm_sendPss — encrypted point-to-point to a recipient's PSS public key.
  • swarm_sendGsoc — topic broadcast; any subscriber to the derived GSOC address receives it.
  • swarm_getMessagingIdentity — discloses the origin's PSS public key + overlay so peers can address it.

It reuses the core spec's consent model (a new messaging permission tier), origin-scoping, base64 payload encoding, and error format, and is purely additive — feature-detected via getCapabilities().features.includes("messaging").

A few design points I'd especially value feedback on:

  1. The streaming shape. I went with EIP-1193's subscription-id + message event rather than per-method callbacks, to keep a single streaming idiom. Reasonable, or is there a preferred convention?
  2. GSOC send as a distinct method rather than a mode of swarm_writeSingleOwnerChunk — because GSOC needs a mined owner key for neighborhood placement, not the origin's fixed signing identity. I think a separate method keeps the core SOC-write contract clean, but I'm open to folding it in.
  3. Delivery semantics. I made "best-effort, unordered, at-least-once" normative and prominent, steering applications toward CRDT/idempotent designs and toward the durable feed layer for authoritative state (feeds = source of truth, messaging = live propagation).
  4. GSOC overwrite behavior for delta streams — I flagged that where every update matters, senders should vary the address per message or use PSS, since a GSOC address holds a single latest value. Want to make sure that framing matches implementers' expectations.

This isn't speculative — it's designed against a working light-node PSS/GSOC implementation (send and receive on a light node, interop-tested against bee and verified on mainnet), and an informative "Reference Endpoints" section maps each method onto the gateway endpoints that back it. Happy to split it into its own PR if that's cleaner for review; I kept it here since it directly extends this spec.

…losure, error/table fixes

- GSOC topic→address: determinism is now scoped honestly to a single
  implementation; cross-provider convergence requires a derivation profile
  (identifier, target, mining order, proximity) that is deferred to Future
  Work. Cross-provider apps exchange the resolved address and use the raw
  address param. Test case relabeled accordingly.
- getMessagingIdentity no longer discloses the full node overlay (a
  node-global, cross-origin correlation handle that pinpoints the node's
  neighborhood); it returns a truncated pssTarget prefix (<= maxTargetDepth
  bytes) instead. sendPss targets is now required and documented as the
  recipient's pssTarget. New Security Considerations entry on cross-origin
  identity correlation.
- Add missing invalid_target row to the -32602 reason table (was referenced
  by swarm_sendPss but never defined).
- Mid-stream failure text no longer suggests emitting an error inside the
  message event (whose schema has no error variant); implementations MUST
  document degraded-pipeline behavior, normative signalling stays in Future
  Work.
- Dedup note: byte-identical sends on the same key are indistinguishable at
  the transport layer and MAY coalesce; apps needing distinguishable repeats
  include a nonce/sequence.
- features/limits MUST (not MAY) be available pre-grant, matching the core
  spec's permission-free getCapabilities; SHOULD-level per-origin send rate
  cap under auto-approve.
@flotob

flotob commented Jul 14, 2026

Copy link
Copy Markdown
Author

A self-review pass on the messaging draft turned up a few things worth fixing before anyone else hit them — pushed as 49eab20:

  1. GSOC topic→address interop, stated honestly. The draft required deterministic derivation and gestured at bee-js gsocMine for cross-implementation interop — but determinism alone doesn't pin the address. Two conforming providers must also agree on the identifier derivation, the target-neighborhood derivation, the mining search order, and the required proximity; differing on any one yields a different address for the same topic. The spec now guarantees convergence within an implementation, says cross-provider apps exchange the resolved address (returned by both swarm_subscribe and swarm_sendGsoc) and use the raw address param, and defers a normative derivation profile to Future Work. This is also the design question I'd most value input on: should v1 pin a normative topic→address profile (so "room:doc-42" means the same address everywhere), or is the address-exchange escape hatch acceptable until implementations converge on one?

  2. swarm_getMessagingIdentity no longer returns the full node overlay. The overlay is node-global — identical for every origin — so returning it would hand any two messaging-granted dApps a cross-origin correlation handle that also pinpoints the user's node's network position. PSS routing only needs a prefix, so the method now returns a truncated pssTarget (≤ maxTargetDepth bytes); swarm_sendPss's targets is now required and documented as the recipient's pssTarget. New Security Considerations entry covers the correlation reasoning.

  3. Smaller normative fixes: added the missing invalid_target row to the error-reason table (it was referenced but never defined); reworded mid-stream failure signalling, which previously suggested emitting an error inside the message event whose schema has no error variant (implementations MUST document degraded-pipeline behavior; normative signalling stays in Future Work); noted that byte-identical sends on the same key are indistinguishable at the transport layer and may coalesce (apps needing distinguishable repeats include a nonce/sequence); features/limits are MUST-available pre-grant, matching the core spec's permission-free getCapabilities; and a SHOULD-level per-origin send rate cap under auto-approve, since sends consume postage.

None of this changes the four methods or the streaming shape — it tightens claims to what the mechanics actually guarantee.

Two gaps surfaced by hardening the reference light-node implementation:

- Empty payloads: swarm_sendGsoc data is now 1..maxMessageBytes with a
  new invalid_payload reason — a GSOC update is a single-owner chunk and
  bee's chunk layer rejects an empty SOC payload, so the provider must
  not sign a chunk the network refuses. swarm_sendPss explicitly ALLOWS
  empty data (the trojan framing carries an explicit length; zero-byte
  pings are well-formed). Test cases added for both.

- Node capacity != per-origin cap: receive pipelines are a node-wide
  pool (the reference implementation bounds distinct concurrently-
  pulled neighborhoods across ALL origins), so subscribe can fail
  because OTHER origins hold the slots. That case MUST surface as a
  retryable 4900 naming subscription capacity — not
  too_many_subscriptions, which stays reserved for the per-origin
  maxSubscriptions the dApp itself can fix by unsubscribing. Limits
  table notes why the aggregate bound is not advertised per-origin.
@flotob

flotob commented Jul 15, 2026

Copy link
Copy Markdown
Author

Small follow-up to the messaging draft (19cfb9d): hardening the reference light-node implementation surfaced two spec gaps worth closing before they bite an implementer.

  1. Payload bounds were only half-specified. The draft bounded data from above (payload_too_large) but said nothing about empty payloads — and the two transports genuinely differ there: a GSOC update is a single-owner chunk, and bee's chunk layer rejects an empty SOC payload, so an empty broadcast can't be expressed and the provider must not sign a chunk the network refuses (swarm_sendGsoc is now 1..maxMessageBytes with a new invalid_payload reason). PSS, by contrast, carries an explicit length in the trojan framing, so a zero-byte message is well-formed — swarm_sendPss now explicitly allows it (handy for pings/presence signals). Test cases added for both directions.

  2. Node capacity ≠ per-origin cap. too_many_subscriptions covers the origin exceeding its own maxSubscriptions — the dApp caused it and can fix it. But a receive pipeline is a node-wide resource: an implementation bounds how many distinct neighborhoods it will pull concurrently across all origins (our reference implementation allows 8), and that pool can be exhausted by other origins. The draft now requires that case to surface as a retryable 4900 naming subscription capacity — not as a parameter error — so dApps back off or degrade to feed polling instead of treating it as a permanent refusal. The limits table notes why the aggregate bound isn't advertised per-origin (it's shared state, not a promise any one origin can hold).

Both came out of adversarial review of the implementation rather than armchair speculation, which is exactly the feedback loop this draft is meant to have with running code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants