The logic for serving and storing associations between users and apps
TypeScript 66%
Swift 14%
Kotlin 11%
HTML 5%
JavaScript 4%
<1%

README.md

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 postMessage list
  • Safari / Firefox / iOS: Storage Access API when possible; otherwise first-party /save popup, /enroll after 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:

  1. If hasStorageAccess() is already true, list and heartbeat silently.
  2. Else try requestStorageAccess() without a click (Chrome/Firefox can reactivate an existing grant) and reload.
  3. Else show Continue. The click calls only requestStorageAccess() — not hasStorageAccess(), 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 #

  1. On Honks, click Heartbeat.
  2. On Blacksky, the example renderer should list the DID (bsky.app’s DID resolves to a handle).
  3. Click it to see a mocked OAuth start.

Safari / Firefox / iOS #

  1. On Honks, click Heartbeat. If third-party cookies are blocked, a first-party /save window should open (or click Enroll, then Save).
  2. On Blacksky, click Continue with an AT Proto account you already use in the embed (this may prompt for storage access).
  3. Pick the DID in the loggedin.fyi popup.

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.