Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ contributors. This page routes deeper work without duplicating it.
## System guides

- [Farcaster integration](farcaster-integration.md)
- [Reliable Farcaster Mini App notifications](farcaster-notification-playbook.md)
- [Auth bridge](../services/auth-bridge/README.md)
- [SpacetimeDB module](../spacetimedb/README.md)
- [Community Marks policy](gameplay/marks-policy-v1.md)
Expand Down
98 changes: 71 additions & 27 deletions docs/farcaster-integration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Farcaster authentication

For a shorter, reusable account of the notification lifecycle, failure modes,
and production canary process, see
[`farcaster-notification-playbook.md`](farcaster-notification-playbook.md).

Warpkeep supports two presentations of the same server-owned identity boundary:
ordinary browsers use Sign In with Farcaster (SIWF), while a verified Farcaster
Mini App host may use Quick Auth. Neither path is a wallet connection,
Expand Down Expand Up @@ -313,9 +317,10 @@ The control is staged behind the exact public presentation gate
`VITE_WARPKEEP_ADMISSION_NOTIFICATIONS_ENABLED`. Only the literal value `true`
enables it; the checked-in example and Pages fallback are `false`. This gate
does not enable delivery, alter Quick Auth, submit another access request, or
change admission. The repository currently contains no privacy-safe record of
the required genuine production signed add/enable and disable/remove canary, so
the gate must remain false until the owner completes and records that review.
change admission. An owner-controlled production canary completed on 4 August
2026 and visibly received the Farcaster alert. That observation establishes the
canary outcome, not universal device delivery; presentation rollout remains a
separate deliberate configuration choice.

The host adapter projects add/remove, enable/disable, rejection, invalid
manifest, timeout, and setup-pending outcomes into a closed presentation state.
Expand All @@ -333,20 +338,46 @@ with bounded retries. When both RPCs answer they must agree; after one exhausts
its transport retries, the healthy independent view may complete verification
and emits only a static degraded-mode event. Conflicting answers, two failed
transports, or inactive on-chain state still fail closed. Delivery is accepted
only for the exact configured client FID and URL. A valid disable/remove
remains usable while outbound delivery is paused so opt-out cannot be trapped
behind a feature gate. Raw notification tokens stay inside one private
only for the exact configured client FID and URL. Valid add/enable and
disable/remove events remain usable while outbound delivery is paused: consent
is not lost during a rollout pause, and opt-out cannot be trapped behind a
feature gate. The pause still clears or rejects every outbound send. Raw notification tokens stay inside one private
Cloudflare Durable Object per FID, never in React, browser storage, logs, URLs,
public state, or SpacetimeDB.

Before Hermes requests administrator authority or mutates admission, it calls a
separate-secret operator endpoint for the exact pending access-request
timestamp. The Durable Object proves that request is still pending and that
admission is not enabled immediately before sending. For an opted-in player,
Hermes proceeds only after Farcaster reports the matching token in
`successfulTokens`; without notification consent, it records the explicit
`not-subscribed` result and may proceed. Provider acceptance proves handoff to
Farcaster, not device display or that the player opened the alert.
Hermes first inspects the exact pending access-request tuple, then disconnects
its short-lived administrator session before calling the separate-secret
notification endpoint. For an opted-in player, Farcaster provider acceptance
creates an `awaiting-client` intent; it does not admit the player. The alert
opens an unguessable fragment capability, which is scrubbed from the URL before
rendering. Acknowledgement requires fresh same-FID Quick Auth plus the exact
Farcaster launch context whose notification ID matches the stored intent. The
Durable Object compares both the fragment ticket and that launch ID. If a
same-document WebView exposes a new fragment alongside stale host context, the
client retains the one-use ticket in memory and asks the player to close the
view and reopen the exact alert. Hermes waits for `client-acknowledged`, mints
a new five-minute administrator session, and re-reads the unchanged request
tuple before invoking a request-CAS reducer.
`queued`, `not-subscribed`, `delivery-exhausted`, a legacy receipt, expiry,
identity mismatch, or a changed request all fail closed without admission.
Provider acceptance alone proves only handoff to Farcaster, not display or an
authenticated player open.

For the bounded provider-accepted-but-unopened exception, an operator may use
`reissue-admission-notification <fid> --confirm`. This is a distinct serialized
transition, never an ordinary queue poll. It rechecks the exact disabled
admission and unchanged request timestamp, rotates the one-use intent, ticket,
and notification ID atomically, enforces a five-minute cooldown, and caps the
request cycle at two reissues. The cap and latest provider/client receipts live
in a private token-free sidecar, leaving the established pending-delivery and
reviewed grant record shapes unchanged for rollback. Acknowledgement and reissue share the same
per-FID serialization: acknowledgement-first blocks reissue, while
reissue-first makes the prior capability stale.

The request-CAS reducers enforce admission kind and the exact request
cycle/timestamp. The reviewed Hermes workflow supplies the notification-open
gate; the general administrator role remains intentionally capable of other
privileged maintenance and is not cryptographically restricted by this flow.

Queue-before-webhook races are retained without a token for at most 24 hours,
signed opt-outs erase token material immediately, invalid tokens are purged,
Expand All @@ -360,29 +391,42 @@ The reviewed payloads are:

```txt
normal admission:
notificationId: warpkeep-access-approved-v2-r<pending-request-timestamp>
title: Admission approved
body: The Hegemony is finalizing your Realm access. Your keep will open shortly.
notificationId: warpkeep-access-grant-v3-i<random-intent-id>
title: Welcome to the Hegemony Empire
body: The gates have answered your name. Cross the threshold, Founder—your legacy awaits.
targetUrl: https://warpkeep.com/?miniApp=true#warpkeep-grant-v1=<one-use-ticket>

already-live reconciliation:
notificationId: warpkeep-access-approved-v1-e<positive-auth-epoch>
title: The Hegemony admits you
body: Your keep awaits in Genesis 001. Enter the living Realm.
title: Welcome to the Hegemony Empire
body: The gates have answered your name. Cross the threshold, Founder—your legacy awaits.
targetUrl: https://warpkeep.com/?miniApp=true
```

The titles and bodies are within Farcaster's bounds, contain no identity or
private state, and accurately describe their generation. Copy changes require
a reviewed Worker rollout.

For a notification launch, the browser retains only
`location.type === "notification"` and a notification ID matching either
`warpkeep-access-approved-v2-r<positiveInteger>` or the rollback-compatible
`warpkeep-access-approved-v1-e<positiveInteger>` within the 128-character
limit. Host title and body are discarded. Warpkeep then shows a short
confirmation state and runs normal Quick Auth, current admission, Terms, and
canonical-keep checks. A pending or changed account stays pending; the
notification itself never grants access or creates another keep.
For a normal admission launch, the browser captures only the exact
`warpkeep-grant-v1` fragment ticket, removes it from visible history immediately,
and keeps it in one module-local call stack. It never enters React state,
storage, analytics, logs, or a network request other than the authenticated
grant endpoint. The bridge re-verifies the signed FID and current request; the
Durable Object accepts an exact one-use ticket only after provider acceptance.
It also requires the exact `warpkeep-access-grant-v3-i…` ID from Farcaster's
immutable notification launch context; a ticket paired with launcher, legacy,
malformed, or different notification context is rejected without admission.
The frontend then polls admission at a bounded cadence while Hermes performs
the fresh request-CAS mutation. A stale or changed account remains pending, and
the notification itself cannot create a keep, bypass Terms, or grant gameplay
authority. Legacy admitted-epoch notifications retain their existing entry
behavior but cannot authorize the new pending-request flow.

This exchange is served only at the additive
`/v2/access/admission-grant-context` path. The Worker must land before the
matching frontend; the live predecessor has no grant client, and the new client
never retries against the discarded ticket-only candidate path. That ordering
keeps cached predecessor bundles inert while the route converges.

**CHECK ADMISSION** is a typed, read-only presentation around credentialed
`/v2/session/refresh`, not a new Farcaster channel or access-request mutation.
Expand Down
Loading