| Version | Supported |
|---|---|
| 1.20.1+ | ✅ |
| 1.19.0+ | ✅ |
| 1.18.0+ | ✅ |
| < 1.17 | ❌ |
To report security issues send an email to security@fedi.xyz (not for support).
The bridge derives the stable FA key-family root at child id 17 of the app root
and opens fa-client inside the globally reserved database prefix 0x07.
Manifold owns the protocol-signing, Nostr-author, and backup-encryption children
below that scoped root. Changing either scope changes durable identity or
storage ownership and requires an explicit migration. For a restored mnemonic in
a fresh FA database and selected Manifold environment, fa-client checks the
canonical relays before allowing a new formation. It claims its mutation guard
before returning from open, and Fedi holds its RPC command queue until recovery
completes. Manifold records a completed restored-or-no-backup check in its FA
namespace, scoped by the environment within that database; a fresh installation
or environment needs a new check. Authenticated import and completion commit
atomically, and an internal FA reset clears both. Relay failures and incomplete
reads never complete the check: they remain visible as retryable recovery state
and keep FA creation gated without failing bridge startup. A successful empty
result is a deployment-policy quorum of complete relay reads, not proof of
absence on every relay.
Fedi commits an internal environment switch and its queued FA wipe in one transaction. Switching away from Production is refused, and a namespace without a known non-Production owner cannot be wiped. Production selection is intentionally one-way even before a queued switch runs: this seed can have a Production formation on relays even when its local namespace is empty.
On first upgraded internal startup, the legacy app-state selection (or the build default) moves into the bridge database. A nonempty ownerless namespace receives that owner only when no wipe is pending: the previous implementation queued a wipe before changing the selection and applied it before opening FA. An ownerless legacy pending wipe remains ambiguous and blocks FA startup without deleting data. There is no in-app escape from that state; reinstall is destructive and must not be presented as a value-safe recovery operation.
Pinned Manifold Guardian locators are untrusted dialing input plus the public key used to verify manager commitments. Parsing a locator is not a trust verdict. Every Iroh connect and Manifold Guardian request must remain deadline-bounded, and responses that carry commitments must be verified against the locator key.
Mutating FA formation work is owned by Runtime.task_group, serialized to one
driver, and cancellable at bridge shutdown. RPC caller cancellation must not
detach an untracked formation future. The eligible-payer read is the deliberate
exception: it performs a bounded, read-only authenticated policy refresh in the
RPC caller future and intersects that result with fully loaded joined wallets.
Conversely, cancelling the task may leave durable protocol effects; launch
recovery must reopen the same database and call resume so exact quotes and
wallet operations are reconciled rather than replaced or spent twice. This
includes an unsynced persisted Formed record: it stays projected as Formed,
reports the launch recheck through freshness, and is resumed until the MG
directory reaches consensus before fresh terminal success is exposed.
Explicit abandonment is a no-argument mutation serialized through that same
driver. The bridge delegates the value-safety decision to fa-client and must
never delete formation state directly: abandonment succeeds only before wallet
output generation is durably armed and before the federation is formed. Before
releasing the durable mutation's driver claim, Fedi invalidates every local
selection and replacement approval and detaches its unowned formation push
hook. This cleanup belongs to the driver so it still runs when either the paid
formation caller or the abandonment caller has disconnected. A durable
Formation to Idle transition is authoritative even if the subsequent
driver-lease release reports an error, so that error cannot retain stale local
authorization.
Internal dev, test, and staging builds have one explicit test-only exception.
Developer Settings may schedule the non-Production FA namespace for deletion
on the next launch, before fa-client or its driver opens. The reset
marker lives outside that namespace and the marker plus namespace are cleared
atomically. Edge and production builds must neither schedule nor apply it. This
is a destructive test wipe, not abandonment or remote cleanup: in-flight Fleet
Manager or liquidity work may continue, and payer-wallet holds and journals in
joined federation databases are deliberately left untouched and may remain
unavailable. The UI must say so and direct testers to disposable wallets.
The Manifold push-gateway client is cached with the Manifold deployment of the runtime environment that supplied its feature configuration. Registration, unregistration, and formation hooks may use that client only while the same deployment is selected. Switching an internal build to another deployment must fail push setup closed rather than send installation credentials or callback traffic to the previously configured gateway.
FA status snapshots are sensitive application state. They can contain Fleet Manager locators, guardian/DKG codes, invite codes, quote-bound authorization details, and exact payment terms. Neither handled nor orphaned stream payloads may be written to routine UI logs or exported support logs. Stream diagnostics must be limited to non-payload metadata such as stream id, sequence, method, and whether a handler existed; the subscribed callback still receives the full typed value in memory.
Post-formation liquidity uses a second network-facing boundary. Provider
advertisements are untrusted until fa-client verifies the signed Nostr event
and payload, the advertised provider identity, a current PeerBadge holder
authorization for the selected Manifold environment, and the signed Iroh node
identity. BridgeLiquidityConnector then dials that exact node with only the
Public Liquidity API ALPN; transport success is not a trust verdict. Discovery
is a fresh, bounded, no-private-data operation. The federation invite is passed
to a provider only inside fa-client, after the complete admission gate is
repeated immediately before the request. The bridge must not log the invite,
signed request, trust material, or endpoint capabilities. Operation snapshots
also contain provider identity, endpoint hints, amount bounds, semantic hashes,
and progress; exclude them from routine UI and support logs. Provider-private
failure reasons do not cross the RPC projection.
Exported FLIP diagnostic logs may include only a provider's public identifier, a FLIP request identifier, an enumerated operation status or outcome, and a bounded protocol or application error code. Those fields may correlate a request with its outcome, but do not authorize logging an operation snapshot or any private payload. In particular, support logs must exclude invite codes, credential documents, endpoint capabilities, raw requests, response payloads, and free-form error text.
Liquidity start and resume mutations share the task-group-owned FA driver,
so dropping an RPC caller discards only its response while accepted work keeps
one serialized owner. Bridge shutdown may cancel an in-flight call only at the
Manifold durable checkpoint. The exact semantic operation remains available
through bounded, read-only status, canonical current, and cursor-paginated
list RPCs. On launch, the same driver reads Manifold's canonical current
operation and resumes that existing id; it must never invent a replacement
request after a lost response. Projected transport and provider errors are
sanitized. Item state and completion evidence remain provider-authoritative
and must be verified against the joined federation before the app presents
liquidity as ready. actionRequired is surfaced for an operator decision, not
converted into an automatic retry of a potentially irreversible provider
operation.
Completed gateway liquidity is not ready until fa-client registers the exact
validated gateway URL with the guardians and a fresh threshold-aggregated LNv2
read contains it. BridgeFederationConsensusReader must perform that real,
uncached read through the invite code; fabricating or caching membership would
silently defeat the post-write verification. The RPC projection exposes only
the resulting gatewayViewVerified boolean, never the gateway capability.
The Fedi wallet adapter (bridge::fa_payments) implements exact quote
recovery and idempotent refund settlement for paid formation through the pinned
Manifold contract. Production app RPCs expose only the verified selection
preview followed by explicit-payer Pay & create. If one exact paid row
is later proven safe to replace, production exposes only a fresh verified
replacement preview followed by its sealed approve/apply call. That one renewed
user action durably sets the replacement subset's cap; exact quotes at or below
it auto-authorize. A larger exact total parks before output signaling and can
continue only through the narrow exact-subset replacement-authorization RPC;
Manifold rejects that RPC for initial selected creation and every other state.
Formation always uses
zero guardian fee. The product-default 0.5% guardian fee is a distinct
post-formation maintenance operation, after Formed. That RPC accepts only a
rate. Manifold parses the exact federation id from its persisted formed invite
and passes it to the bridge's local fee-account capability; the bridge resolves
that already joined, Ready federation and returns
spv2.our_account(AccountType::BtcDepositor). UI, RPC, and driver messages must
never carry an alternate FA recipient account. Missing joined state or SPv2
capability fails before any guardian vote, while Manifold retains account-shape,
role-separation, recipient-policy, and consensus-readback validation. FA RPCs
expose stable typed error categories; UI code must not branch on error strings,
and payment errors surfaced through the port must not carry remote or
wallet-internal detail.
Seat payments hold these invariants. A per-quote journal record in the paying
federation's client database (tag 0 under prefix 0xce, FaSeatPayment;
0xcc and 0xcd remain owned by the LNURL event consumer) commits in the same
database transaction as the funding transaction's submission state machines, so
recoverability is durable strictly before any network submission; an absent
record is proof that no funding began once the exact reservation member has
been found. This remains true for a client created through recovery because
reservation consumption and journal creation commit together. After a restart,
the adapter has to work out where a payment got to before letting anything move
forward. The rules:
- Accepted by the federation: replay the saved evidence.
- Rejected: not final yet. The notes the payment tried to spend are refunded automatically, and we wait until every one of them is back in the wallet and spendable before reporting "rejected" and allowing a replacement. Reporting it early would let a new payment start before the money is back.
- Timed out: never treated as rejected. We just try again later.
- Accepted isn't "done" either: we also wait for the payment's own change to be back and spendable before reporting the payment prepared.
Once a journal exists, it must agree with the operation metadata about the quote, transaction, and which outputs are our change. Missing or conflicting operation metadata stops recovery rather than guessing — recomputing the change range after a restart could point at the wrong transaction.
Refund tracking only uses the wallet's own transaction history, matched by transaction id because updates can repeat or arrive out of order. The accepted refunds must return exactly the notes the payment spent — nothing extra, no note twice. (On mint v1 the refund may arrive as one bundle or as one transaction per note.) Anything partial, malformed, or contradictory stops recovery and leaves the payment resumable.
Refund key material is re-derived from the paying federation's auxiliary secret
(child id 18) and the quote's public refund nonce; it is never persisted
anywhere. Refund settlement atomically claims a reverse index keyed by
refund-issuance hash (tag 1 under prefix 0xce, FaSeatPayment) and records
a claim hash in the quote row before crediting,
so concurrent sibling settlements cannot both own one deterministic issuance.
It refuses a hash already claimed by another quote; exactly-once crediting
additionally depends on the MG re-presenting the byte-identical refund
transaction when it retries a refusal for the same quote. A distinct credited
hash is written only after deterministic mint receive reaches terminal success;
the earlier claim is never sufficient proof for releasing/replacing a seat.
Aggregate readiness owns an opaque durable virtual-wallet reservation (tag 0
under prefix 0xcf, FaFunding) through Manifold's
payment_outputs_started tombstone and every journaled submission commit.
An O(1) active total (tag 1 under prefix 0xcf, FaFunding) is updated in
the same transaction as every reservation/member transition; lifetime
settlement tombstones are therefore never scanned on ordinary balance reads.
FederationV2::get_balance subtracts every live FA reservation, so all ordinary
wallet sends continue to exclude that value after an app crash/restart. The FA
owner sees only globally available virtual balance plus its own reservation.
The row binds the exact signed quote/output plans and conservative fee ceilings
and retains one settlement tombstone per quote. Each submitted quote consumes
only its own member and removes that member from the active hold total in the
same transaction as its payment journal. Only a still-Held member may be
released for replacement after the wallet proves the exact reservation member
exists and no journal exists. Consensus rejection or a settled signed refund may
authorize Manifold to replace a submitted seat, but the local member stays
Consumed; terminal release validates that tombstone and never restores held
value. A rejected funding transaction's automatic input refunds and an MG-signed
payment refund are separate deterministic wallet credits, and both must be spendable
before their respective terminal proof exists. Dropping
Manifold's id/capability never releases funds, and accepted, pending, ambiguous,
and sibling members remain untouched. Authorization does not pick notes or build transactions; it only
reserves each seat's amount plus a fee allowance. That's deliberate: one
large note can pay several seats in a row through its change, and change
only exists once a real transaction is accepted. Each payment is built
one at a time under the spend lock, and its actual cost must fit inside
the seat's reserved amount.
After finalization, but before the funding database transaction commits, the
adapter reads the just-written transaction-submission state and calculates the
exact wallet debit as selected mint inputs minus returned change for mint-v1 or
mint-v2. That exact debit must fit both the captured virtual balance and the
reservation; consuming the reservation, journaling the quote, and publishing
the submission state machines are one atomic commit. It must not fund a seat
from accrued Fedi-fee reserves merely because physical balance is larger.
The FA database stores resolved intent, locators, signed public quotes,
quote-bound aggregate authorization, seat ids, guardian codes, and the final
invite. It must never contain identity secrets, raw ecash, payment signatures,
or refund secrets; seat-payment journals and refund derivations belong to the
paying federation's own client database and secrets. The adapter and its
projected RPC values must compile for native mobile targets and
wasm32-unknown-unknown.
Once the user authorizes the exact ecash outputs for a paid formation, the
funding federation records a durable virtual-balance hold before payment work
continues. The hold and its O(1) active total use distinct tags in prefix 0xcf
of that selected payer federation's local Fedimint client database. Payment
journals and reverse refund claims likewise use distinct tags in prefix 0xce.
They do not
live in the globally namespaced FA database, whose formation state may outlive
or be restored independently of this payer-wallet evidence. Both rows are
lifecycle state: changing their encoding, keys, or transition rules requires
either an explicit migration or a coordinated pre-launch reset for every test
or internal installation that may resume across revisions.
The pre-rebase FA-only 0xcb-0xce layout is not migratable after master
assigned 0xcc/0xcd to LNURL delivery state; internal installations from
that unreleased layout require a coordinated reset before using this encoding.
An ordinary process restart reopens the same payer database and must retain the
hold, active total, and every terminal member state. A seed restore, backup
restore, or federation rejoin can produce a new local payer database without
those rows even when restored FA state remembers the formation. A missing local
hold remains ambiguous, so fa-client fails before seat recovery. Once the
exact hold and member exist, a missing journal proves that member has not
started because consumption and journal creation are atomic. Consumed and
ReleasedUnstarted members are durable replay tombstones. They must not be
deleted or compacted unless a coordinated rule proves that no surviving FA
state, quote, journal, or refund can refer to their aggregate token.
All balance checks and hold transitions are serialized by the funding
federation's spend lock. FA APIs accept a federation-branded guard so a lock
from one joined federation cannot authorize another federation's funds. The
atomic-consumption API also creates the database transaction from that exact
payer and passes callers a non-forgeable wrapper; it never accepts an arbitrary
raw transaction that could come from a different federation. A member has an
explicit Held, Consumed, or ReleasedUnstarted state. A positive bounded
debit moves Held to Consumed in the same database transaction that submits
the wallet funding operation. Only Held may be released without a payment
journal; consumed value is never restored by this layer. Exact retries are
idempotent and changed fingerprints, quote sets, or debits fail closed. Every
committed change to the projected wallet balance refreshes the normal
federation balance event. Successful exact retries also refresh it, so
cancellation after commit but before notification heals on the next retry.
Consumption returns a private balance-change token that the caller must emit
only after its transaction commits successfully. Emitting before commit
recomputes the old balance and does not satisfy the post-commit refresh; the
caller must retain the token across commit and emit it afterward.
FA notification ownership is a third deterministic key family, separate from
the FA protocol key and Fedi's social Nostr key. The bridge reserves global
root-secret child id 19, then domain-separates it with
fedi-push-gateway/recipient-auth-nostr/v1 and the canonical Manifold
environment before deriving the Nostr key used for NIP-98 management requests.
Changing the child, label, or environment mapping or reusing another Nostr key
would orphan or link gateway ownership and requires an explicit migration.
Development/test, staging, and production identities must remain distinct.
The bridge pins the public recipient key produced for all three canonical
environment mappings in executable golden tests. A change to any golden is an
identity migration—not an ordinary refactor—and must ship with an explicit
gateway registration/hook ownership migration before the derivation changes.
The stable, at-most-128-byte DeviceIdentifier is the gateway installation id.
The native layer supplies the current FCM token and platform through the
registration RPC; it never supplies recipient identity, installation identity,
gateway URL, callback URL, or a signing key. FCM tokens and callback URLs are
secret-adjacent bearer material: do not format them, return them over RPC, add
them to error messages, or follow redirects while sending them. HTTP response
bodies are bounded and only a constrained gateway error code may cross the RPC
boundary. Each NIP-98 request includes a signed random nonce so identical
same-second operations do not collide in the gateway replay cache. Production
accepts only a compiled HTTPS root origin and has no
end-user override; missing configuration fails push setup before any real
quote or wallet output is requested.
Browser/WASM builds retain the RPC types but cannot construct a push-gateway
client: they have no native FCM installation and their Fetch-backed reqwest
transport cannot enforce the no-redirect, bounded-streaming contract above.
They therefore return the same sanitized unavailable result before any gateway
request, quote, or wallet output instead of weakening that boundary.
Installation registration is deliberately independent of FA selection and may
happen before a quote exists. It proves only recipient-key possession plus a
gateway-validated Fedi-project FCM token, never a human identity or financial
authority. Push payloads therefore contain only a generic FA workflow/action.
They must not contain or authorize payment, progress, an invite code, a
federation mutation, or any decision. On tap, the UI reopens the FA surface and
uses authoritative status/recovery to choose the closest durable state.
The exact routing fields are governed in HACKING.md and implemented by the
single strict parser in ui/common/utils/faPush.ts; consumers must not create a
second string contract.
At Pay & create, the bridge attempts to create one installation-scoped,
one-use, 30-day hook before entering Manifold's paid operation; unavailable
push setup does not block formation. The callback is passed
only through the callback-aware pay_and_create method, which must persist it
in the formation initialization transaction before external effects. A later
setter is forbidden. Fedi does not duplicate the callback in its database; a
crash before Manifold initialization can leave only an expiring gateway orphan.
Once a formation exists, Manifold is the sole durable owner and gateway failure
must never roll back money or DKG. A hook may be revoked by Fedi only after the
driver proves no durable formation owns it.
This callback behavior must not deploy until the Manifold #69 fix is deployed to every Fleet Manager the app can select. Merging or pinning the fix in the client library does not establish server compatibility. A fixed manager ignores a supplied callback when callback delivery is unconfigured and continues DKG, but still rejects a configured callback that is malformed or has the wrong origin.
The gateway response is accepted only when expires_at == created_at + 30 days
without integer overflow and created_at is fresh relative to the bridge's
trusted request-start/response-receive timestamps. The permitted gateway clock
skew is five minutes in either direction; stale, excessively future-dated, or
locally reversed request/response timestamps fail closed. Exact recipient,
installation, one-use, notification, routing, future expiry, and callback
origin/path checks also apply. A missing, shorter, longer, overflowing,
malformed, stale, future-dated, or cross-origin capability fails before paid
dispatch.
The bridge retains the same validated callback across a safe payer retry. For
an observed driver response, the driver returns its global operation claim to
the callback coordinator, which releases it only after callback ownership and
best-effort cleanup are settled. If the RPC caller disappears before that
handoff, the driver still completes; the next preview must consult
authoritative fa-client formation status before it may revoke the abandoned
local hook. Pre-initialization terminal cleanup is best-effort, while any
durable formation status forbids revocation. Executable production-coordinator
and fake-gateway tests cover response handoff, caller-loss recovery, safe payer
retry, and both sides of this ownership boundary.
Every selected federation manager receives the same callback and stable
idempotency key.
Gateway idempotency must survive for at least the hook's terminal lifetime so
delayed/restarted guardians cannot enqueue a second push after event cleanup.
Federation-manager and fa-client storage must erase the live bearer from
their logical records as soon as their respective recovery ownership ends,
while treating database pages, WAL, and backups as sensitive until normal
storage retention or compaction removes historical bytes. Retryable delivery
failures use bounded backoff; definitive gateway lifecycle/policy rejection
clears the federation manager's capability into a non-secret terminal outcome.
Missing/mismatched operator
origin is blocked configuration, not proof of terminal rejection: retain the
redacted capability without a request loop until configuration is restored or
explicitly migrated.