{"id":50096999,"url":"https://github.com/flo-bit/svelte-atproto-oauth","last_synced_at":"2026-05-23T04:08:29.285Z","repository":{"id":356324335,"uuid":"1232020282","full_name":"flo-bit/svelte-atproto-oauth","owner":"flo-bit","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-07T14:30:49.000Z","size":57,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-07T16:30:18.792Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/flo-bit.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-07T14:07:47.000Z","updated_at":"2026-05-07T14:38:47.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/flo-bit/svelte-atproto-oauth","commit_stats":null,"previous_names":["flo-bit/svelte-atproto-oauth"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/flo-bit/svelte-atproto-oauth","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flo-bit%2Fsvelte-atproto-oauth","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flo-bit%2Fsvelte-atproto-oauth/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flo-bit%2Fsvelte-atproto-oauth/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flo-bit%2Fsvelte-atproto-oauth/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/flo-bit","download_url":"https://codeload.github.com/flo-bit/svelte-atproto-oauth/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flo-bit%2Fsvelte-atproto-oauth/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33382047,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T01:21:08.577Z","status":"online","status_checked_at":"2026-05-23T02:00:05.530Z","response_time":53,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-23T04:08:24.704Z","updated_at":"2026-05-23T04:08:29.277Z","avatar_url":"https://github.com/flo-bit.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `@svelte-atproto/oauth`\n\natproto OAuth for SvelteKit — confidential or loopback OAuth client, with pluggable session storage and slingshot/UFO integration for fast identity + firehose lookups.\n\n```sh\npnpm add @svelte-atproto/oauth\n```\n\nFor one-shot setup of a fresh SvelteKit project, use the [`@svelte-atproto/sv`](https://www.npmjs.com/package/@svelte-atproto/sv) add-on:\n\n```sh\nnpx sv add @svelte-atproto\n```\n\n## Quick start (manual)\n\n```ts\n// src/lib/atproto/index.ts\nimport { createAtprotoAuth } from '@svelte-atproto/oauth/server';\nimport { cloudflareKV } from '@svelte-atproto/oauth/server/stores/cloudflare';\nimport { env } from '$env/dynamic/private';\n\nexport const atproto = createAtprotoAuth({\n  origin: env.ORIGIN,\n  cookieSecret: env.COOKIE_SECRET,\n  clientAssertionKey: env.CLIENT_ASSERTION_KEY,\n  scope: 'atproto',\n  sessions: cloudflareKV('OAUTH_SESSIONS'),\n  states: cloudflareKV('OAUTH_STATES', { ttl: 600 })\n});\n```\n\n```ts\n// src/hooks.server.ts\nimport { atproto } from '$lib/atproto';\nexport const handle = atproto.handle;\n```\n\n```ts\n// src/app.d.ts\nimport type { OAuthSession } from '@atcute/oauth-node-client';\nimport type { Client } from '@atcute/client';\nimport type { Did } from '@atcute/lexicons';\n\ndeclare global {\n  namespace App {\n    interface Locals {\n      session: OAuthSession | null;\n      client: Client | null;\n      did: Did | null;\n    }\n  }\n}\nexport {};\n```\n\nGenerate dev secrets:\n\n```sh\nnpx atproto-oauth setup   # writes COOKIE_SECRET + CLIENT_ASSERTION_KEY into .env\n```\n\nThat's it. `atproto.handle` mounts:\n\n- `GET /oauth-client-metadata.json` — OAuth client metadata\n- `GET /oauth/jwks.json` — JWKS\n- `GET /oauth/callback` — completes the OAuth round-trip\n- `POST /oauth/login` — start a login (returns `{ url }`)\n- `POST /oauth/logout` — revoke + clear cookies\n\nPer request, `event.locals.{ did, session, client }` is populated for any signed-in user.\n\n## Entry points\n\n| Subpath | What it ships |\n|---|---|\n| `/server` | `createAtprotoAuth`, types — server (confidential / loopback) flow |\n| `/server/stores/memory` | `memory()` |\n| `/server/stores/cloudflare` | `cloudflareKV(bindingName \\| namespace, opts?)` |\n| `/server/stores/upstash` | `upstashRedis({ url, token })` |\n| `/client` | `login(handle)`, `signup()`, `logout()` — imperative client helpers for the **server** flow |\n| `/browser` | `createAtprotoBrowserAuth` — full **browser-only** flow for static-site deploys (GH Pages, etc.) |\n| `/helper` | atproto utilities (handle/PDS resolution, listRecords, getRecord, …) |\n| `/bsky` | bsky-specific (`loadBskyProfile`, `loadBskyProfiles`, CDN URL, …) |\n\n## Server API\n\n`createAtprotoAuth(config)` returns:\n\n```ts\natproto.handle                    // mount as your SvelteKit `handle`\natproto.handlers.{metadata,jwks,callback,login,logout}  // raw RequestHandlers\natproto.api.startLogin({ handle, signup?, returnTo? })  // → { url }\natproto.api.logout()              // revokes session + clears cookies\natproto.api.getSession()          // → { did, session, client } from event.locals\n```\n\n### Config (selected fields)\n\n| Field | Default | Notes |\n|---|---|---|\n| `origin` | — | Required outside dev. Empty in dev → loopback (`http://127.0.0.1:5173`) |\n| `cookieSecret` | — | Required outside dev. HMAC for signed cookies |\n| `clientAssertionKey` | — | JSON-encoded JWK; required when `origin` is set |\n| `scope` | `'atproto'` | String or `string[]`. Use atcute's `scope.repo({...})` helpers |\n| `signupPDS` | — | Set to a PDS URL to enable signup; unset = signup disabled |\n| `sessions` | in-memory | Any `Store\u003cDid, StoredSession\u003e` |\n| `states` | in-memory (10m TTL) | Any `Store\u003cstring, StoredState\u003e` |\n| `redirectPath` / `metadataPath` / `jwksPath` / `loginPath` / `logoutPath` | `/oauth/...` | Override path defaults |\n\n## Helpers (`/helper`)\n\nPure atproto utilities — no auth state, no `event.locals` magic. Pass `did` explicitly.\n\n| Function | Notes |\n|---|---|\n| `parseUri(uri)` | AT URI → `{ repo, collection, rkey }` |\n| `resolveHandle({ handle, doh?, slingshot? })` | handle → DID |\n| `actorToDid(actor, opts?)` | handle or DID → DID |\n| `loadMiniDoc(identifier, opts?)` | `{ did, handle, pds }` via slingshot, fallback PLC + describeRepo |\n| `loadHandle(did, opts?)` | DID → handle (cached) |\n| `loadHandles(dids, opts?)` | parallel batch |\n| `getPDS(did, opts?)` | DID → PDS endpoint |\n| `getPDSClient({ did }, opts?)` | unauthenticated atcute Client |\n| `listRecords({ did, collection, ... })` | repo records |\n| `getRecord({ did, collection, rkey?, ... })` | single record |\n| `getRecordByUri(uri, opts?)` | record by AT URI (slingshot or fallback) |\n| `describeRepo({ did, ... })` | repo metadata |\n| `getBlobURL({ did, blob })` | direct PDS blob URL |\n| `recentRecords(collection, opts?)` | recent records by collection from UFO firehose |\n| `countBacklinks(target, source, opts?)` | constellation: like-count, follower-count, etc. |\n| `countDistinctBacklinkers(target, source, opts?)` | constellation: distinct-DID count |\n| `listBacklinks(target, source, opts?)` | constellation: paginated linking records |\n| `listDistinctBacklinkers(target, source, opts?)` | constellation: paginated distinct DIDs |\n| `backlinksRollup(target, opts?)` | constellation: all sources rolled up |\n| `createTID()` | TID rkey |\n| `readThroughCache(cache, key, load)` | the generic cache wrapper used internally |\n\n### Microcosm services (slingshot / UFO / constellation)\n\nBy default, three [microcosm-rs](https://github.com/at-microcosm/microcosm-rs) services back the helpers ([tangled mirror](https://tangled.org/microcosm.blue/microcosm-rs)):\n\n| Service | Default URL | Used by |\n|---|---|---|\n| Slingshot | `slingshot.microcosm.blue` | identity (handle ↔ DID ↔ PDS), `getRecordByUri` |\n| UFO | `ufos-api.microcosm.blue` | `recentRecords` (firehose by collection) |\n| Constellation | `constellation.microcosm.blue` | backlinks (likes, follows, replies, …) |\n\nAll three default URLs are swappable to a self-hosted instance per call:\n\n```ts\nloadHandle(did, { slingshot: 'https://my.host' });\nrecentRecords('xyz.statusphere.status', { ufo: 'https://my.host' });\ncountBacklinks(uri, source, { constellation: 'https://my.host' });\n```\n\nSlingshot also accepts `slingshot: false` — disables the call and falls straight through to the PLC + `describeRepo` fallback (useful when slingshot is degraded for a particular case, or for testing the fallback path). UFO and Constellation have no fallback (no other index gives the same data), so they don't accept `false` — just don't call them if you don't want firehose / backlinks.\n\nAll microcosm calls go through a per-host **circuit breaker** (3 consecutive failures → 60s open, then half-open) and a **5s request timeout**, so an outage fails fast — slingshot skips straight to the fallback, UFO/Constellation return empty/`undefined` immediately instead of hanging.\n\n### Constellation: backlinks\n\nGiven a target (AT URI or DID) and a `{ collection, path }` source describing what links you're looking for, [Constellation](https://constellation.microcosm.blue/) answers \"who linked to this\":\n\n```ts\nimport { countBacklinks, listDistinctBacklinkers } from '@svelte-atproto/oauth/helper';\n\n// like-count for a post\nconst likes = await countBacklinks(postUri, {\n  collection: 'app.bsky.feed.like',\n  path: '.subject.uri'\n});\n\n// follower-count for a DID\nconst followers = await countBacklinks(did, {\n  collection: 'app.bsky.graph.follow',\n  path: '.subject'\n});\n\n// who reposted a post\nconst page = await listDistinctBacklinkers(postUri, {\n  collection: 'app.bsky.feed.repost',\n  path: '.subject.uri'\n}, { limit: 50 });\nconst dids = page?.dids ?? [];\n```\n\n`backlinksRollup(target)` returns a `{ [collection]: { [path]: { records, distinct_dids } } }` rollup of every source pointing at `target` — handy for a \"what does the network say about this\" overview.\n\n## Browser-only flow (`/browser`)\n\nFor static-site deployments (no server runtime — GitHub Pages, Cloudflare Pages without functions, S3, etc.). Tokens live in browser `localStorage`, the DPoP key in IndexedDB. The only thing that needs to be served is a prerendered `oauth-client-metadata.json`.\n\n```ts\n// src/lib/atproto.ts\nimport { createAtprotoBrowserAuth } from '@svelte-atproto/oauth/browser';\n\nexport const atproto = createAtprotoBrowserAuth({\n\torigin: 'https://my-app.example',\n\tscope: 'atproto',\n\tsignupPDS: 'https://pds.rip/' // optional\n});\n```\n\n```ts\n// src/routes/oauth-client-metadata.json/+server.ts\nimport { atproto } from '$lib/atproto';\nimport { json } from '@sveltejs/kit';\nexport const prerender = true;\nexport const GET = () =\u003e json(atproto.metadata);\n```\n\n```svelte\n\u003c!-- src/routes/+layout.svelte --\u003e\n\u003cscript\u003e\n\timport { onMount } from 'svelte';\n\timport { atproto } from '$lib/atproto';\n\tonMount(() =\u003e atproto.init());\n\u003c/script\u003e\n```\n\nIn components:\n\n```svelte\n\u003cscript\u003e\n\timport { atproto } from '$lib/atproto';\n\tconst { user, login, logout } = atproto;\n\u003c/script\u003e\n\n{#if $user.isInitializing}\n\tloading…\n{:else if $user.isLoggedIn}\n\tsigned in as {$user.did}\n\t\u003cbutton onclick={logout}\u003eSign out\u003c/button\u003e\n{:else}\n\t\u003cbutton onclick={() =\u003e login('alice.bsky.social')}\u003eSign in\u003c/button\u003e\n{/if}\n```\n\nIn dev, the lib uses a loopback `client_id` automatically — no public URL or metadata route needed for local testing.\n\n`atproto.user` is a `svelte/store` `Readable` so components consume it via `$user.x` (auto-subscription).\n\n## Bsky helpers (`/bsky`)\n\nOpt-in. Anyone on a custom appview never imports this and pays nothing.\n\n```ts\nimport { loadBskyProfile, loadBskyProfiles, getCDNImageBlobUrl } from '@svelte-atproto/oauth/bsky';\n\nconst profile = await loadBskyProfile(did, { cache });\nconst profiles = await loadBskyProfiles(dids, { cache });  // batched via app.bsky.actor.getProfiles (25/call)\nconst imgUrl = getCDNImageBlobUrl({ did, blob });\n```\n\n## Stores\n\n```ts\nimport { memory } from '@svelte-atproto/oauth/server/stores/memory';\nimport { cloudflareKV } from '@svelte-atproto/oauth/server/stores/cloudflare';\nimport { upstashRedis } from '@svelte-atproto/oauth/server/stores/upstash';\n```\n\nOr implement your own — anything matching atcute's `Store\u003cK, V\u003e` interface works.\n\n`cloudflareKV` accepts either a binding name (looks it up via `getRequestEvent().platform.env`) or a `KVNamespace` directly.\n\n## CLI\n\n```sh\natproto-oauth setup    # idempotent — generates COOKIE_SECRET + CLIENT_ASSERTION_KEY into .env\natproto-oauth secret   # print a fresh COOKIE_SECRET to stdout\natproto-oauth keygen   # print a fresh CLIENT_ASSERTION_KEY (JWK) to stdout\n```\n\nFor production secrets, pipe into your secrets manager:\n\n```sh\natproto-oauth secret | wrangler secret put COOKIE_SECRET\natproto-oauth keygen | wrangler secret put CLIENT_ASSERTION_KEY\n```\n\n## Status\n\nPre-1.0. The public API is small and unlikely to churn, but expect breaking changes until stabilized. Issues + PRs welcome at \u003chttps://github.com/flo-bit/svelte-atproto-oauth\u003e.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflo-bit%2Fsvelte-atproto-oauth","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fflo-bit%2Fsvelte-atproto-oauth","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflo-bit%2Fsvelte-atproto-oauth/lists"}