loggedin.fyi #
A device-local DID hint service for AT Proto apps. Participating clients heartbeat a session cookie; other clients can list those DIDs and start a normal OAuth login. This is not SSO.
- Chrome: hidden embed, silent
postMessagelist - Safari / Firefox / iOS: Storage Access API when possible; otherwise first-party
/savepopup,/enrollafter login, and/choose - Runtime: Cloudflare Worker + one Durable Object per browser session
Storage Access #
The /embed iframe and its XRPC calls use the unpartitioned loggedin_session cookie (SameSite=None; Secure). Chrome speaks Storage Access Headers; Safari and Firefox use document.requestStorageAccess(). Permission is stored as (embedder site, loggedin.fyi) — a grant on Lightbox does not cover Cratewalk.
Parent iframes must set allow="storage-access". Use Loggedin.prepareEmbed / Loggedin.mountEmbed so the listener is attached before src.
Sec-Fetch-Storage-Access |
Worker |
|---|---|
none |
Uncredentialed embed (Continue…). No Set-Cookie (that would orphan Durable Objects). |
inactive |
401 + Activate-Storage-Access: retry; allowed-origin="<parent origin>". Never allowed-origin=*. |
active |
Cookie attached; Activate-Storage-Access: load. |
| missing + same-site / same-origin | First-party: attach cookie (/choose, /save, /enroll). |
| missing + cross-site | Like none for cookie minting. |
Embed boot:
- If
hasStorageAccess()is already true, list and heartbeat silently. - Else try
requestStorageAccess()without a click (Chrome/Firefox can reactivate an existing grant) and reload. - Else show Continue. The click calls only
requestStorageAccess()— nothasStorageAccess(), which can consume the gesture — then reloads. If that fails,{ type: "popup" }and first-party/choose.
GET /save?did=&origin=&action=save|logout is the fallback when storage access is denied. origin must be the calling app, not this service. A hidden SessionBar iframe cannot collect a first grant; it can reuse one after a visible Continue on that same site.
Local probe #
npm install
npm run dev:probe
That starts:
- Worker at
https://localhost:8787 - Honks (probe A) at
https://localhost:3001 - Blacksky (probe B) at
https://localhost:3002
The demo origins use a self-signed cert. Trust the warning once per origin. For HTTP instead:
npx wrangler dev
npm run probe -- --http
Then open http://localhost:3001 and http://localhost:3002.
Chrome #
- On Honks, click Heartbeat.
- On Blacksky, the example renderer should list the DID (bsky.app’s DID resolves to a handle).
- Click it to see a mocked OAuth start.
Safari / Firefox / iOS #
- On Honks, click Heartbeat. If third-party cookies are blocked, a first-party
/savewindow should open (or click Enroll, then Save). - On Blacksky, click Continue with an AT Proto account you already use in the embed (this may prompt for storage access).
- Pick the DID in the
loggedin.fyipopup.
Native SDKs #
iOS and Android packages live in sdks/. Native apps cannot hold the cookie directly, so they reach the browser's shared cookie jar once — ASWebAuthenticationSession on iOS, Custom Tabs on Android — and trade that for a signed token (X-Loggedin-Token) naming the same session. No device identifier, no permission, and the app lands on the same session as mobile Safari or Chrome.
Set the signing secret to enable it:
npx wrangler secret put DEVICE_TOKEN_SECRET
For local wrangler dev, copy .dev.vars.example to gitignored .dev.vars and put a 32+ character value there.
Without it, GET /app/start returns 501 and only the web embed works.
API #
Unauthenticated XRPC. The cookie is the browser session key. Native clients send X-Loggedin-Token instead and never receive a cookie. Writes from the embed, enroll, and save pages also send the parent origin; token writes are pinned to the origin inside the token.
| Lexicon / page | HTTP |
|---|---|
fyi.loggedin.account.loggedIn |
POST /xrpc/fyi.loggedin.account.loggedIn |
fyi.loggedin.account.logout |
POST /xrpc/fyi.loggedin.account.logout |
fyi.loggedin.account.list |
GET /xrpc/fyi.loggedin.account.list → { accounts: [{ did, origins }] } |
| first-party save | GET /save?did=&origin=&action=save|logout |
| native bootstrap | GET /app/start?origin=&callback=&state=[&did=] |
| native bootstrap | GET /app/start?origin=&callback=&state=[&did=] |
Cookie: loggedin_session, HttpOnly, 7-day TTL. HTTPS uses Secure; SameSite=None (Chrome silent embed). Local HTTP uses SameSite=Lax so first-party /enroll, /save, and /choose still work.
Participation: any HTTPS origin that publishes valid OAuth client metadata at /oauth-client-metadata.json. Localhost stays live even if metadata cannot be fetched (self-signed HTTPS). Labels for hinted origins come from GET /origins?origin=…. Abuse controls: Cloudflare rate-limit bindings (IP / origin / metadata fetch), session write coalescing, and a per-session row cap.
Deploy #
npx wrangler login
npm run deploy
Point loggedin.fyi at the Worker in the Cloudflare dashboard.
Val Town demos #
Lightbox (images + feed picker) and Cratewalk (Habbo-style conveyor) live in val-town/. Deploy them as two vals / two origins, set COOKIE_SECRET, and keep each val’s /oauth-client-metadata.json reachable so the Worker can validate them.