npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@svelte-atproto/oauth

v0.3.0

Published

atproto OAuth for SvelteKit. Server (confidential) clients with pluggable session storage. Client (browser) entry coming soon.

Readme

@svelte-atproto/oauth

atproto OAuth for SvelteKit — confidential or loopback OAuth client, with pluggable session storage and slingshot/UFO integration for fast identity + firehose lookups.

pnpm add @svelte-atproto/oauth

For one-shot setup of a fresh SvelteKit project, use the @svelte-atproto/sv add-on:

npx sv add @svelte-atproto

Quick start (manual)

// src/lib/atproto/index.ts
import { createAtprotoAuth } from '@svelte-atproto/oauth/server';
import { cloudflareKV } from '@svelte-atproto/oauth/server/stores/cloudflare';
import { env } from '$env/dynamic/private';

export const atproto = createAtprotoAuth({
  origin: env.ORIGIN,
  cookieSecret: env.COOKIE_SECRET,
  clientAssertionKey: env.CLIENT_ASSERTION_KEY,
  scope: 'atproto',
  sessions: cloudflareKV('OAUTH_SESSIONS'),
  states: cloudflareKV('OAUTH_STATES', { ttl: 600 })
});
// src/hooks.server.ts
import { atproto } from '$lib/atproto';
export const handle = atproto.handle;
// src/app.d.ts
import type { OAuthSession } from '@atcute/oauth-node-client';
import type { Client } from '@atcute/client';
import type { Did } from '@atcute/lexicons';

declare global {
  namespace App {
    interface Locals {
      session: OAuthSession | null;
      client: Client | null;
      did: Did | null;
    }
  }
}
export {};

Generate dev secrets:

npx atproto-oauth setup   # writes COOKIE_SECRET + CLIENT_ASSERTION_KEY into .env

That's it. atproto.handle mounts:

  • GET /oauth-client-metadata.json — OAuth client metadata
  • GET /oauth/jwks.json — JWKS
  • GET /oauth/callback — completes the OAuth round-trip
  • POST /oauth/login — start a login (returns { url })
  • POST /oauth/logout — revoke + clear cookies

Per request, event.locals.{ did, session, client } is populated for any signed-in user.

Entry points

| Subpath | What it ships | |---|---| | /server | createAtprotoAuth, types — server (confidential / loopback) flow | | /server/stores/memory | memory() | | /server/stores/cloudflare | cloudflareKV(bindingName \| namespace, opts?) | | /server/stores/upstash | upstashRedis({ url, token }) | | /client | login(handle), signup(), logout() — imperative client helpers for the server flow | | /browser | createAtprotoBrowserAuth — full browser-only flow for static-site deploys (GH Pages, etc.) | | /helper | atproto utilities (handle/PDS resolution, listRecords, getRecord, …) | | /bsky | bsky-specific (loadBskyProfile, loadBskyProfiles, CDN URL, …) |

Server API

createAtprotoAuth(config) returns:

atproto.handle                    // mount as your SvelteKit `handle`
atproto.handlers.{metadata,jwks,callback,login,logout}  // raw RequestHandlers
atproto.api.startLogin({ handle, signup?, returnTo? })  // → { url }
atproto.api.logout()              // revokes session + clears cookies
atproto.api.getSession()          // → { did, session, client } from event.locals

Config (selected fields)

| Field | Default | Notes | |---|---|---| | origin | — | Required outside dev. Empty in dev → loopback (http://127.0.0.1:5173) | | cookieSecret | — | Required outside dev. HMAC for signed cookies | | clientAssertionKey | — | JSON-encoded JWK; required when origin is set | | scope | 'atproto' | String or string[]. Use atcute's scope.repo({...}) helpers | | signupPDS | — | Set to a PDS URL to enable signup; unset = signup disabled | | sessions | in-memory | Any Store<Did, StoredSession> | | states | in-memory (10m TTL) | Any Store<string, StoredState> | | redirectPath / metadataPath / jwksPath / loginPath / logoutPath | /oauth/... | Override path defaults |

Helpers (/helper)

Pure atproto utilities — no auth state, no event.locals magic. Pass did explicitly.

| Function | Notes | |---|---| | parseUri(uri) | AT URI → { repo, collection, rkey } | | resolveHandle({ handle, doh?, slingshot? }) | handle → DID | | actorToDid(actor, opts?) | handle or DID → DID | | loadMiniDoc(identifier, opts?) | { did, handle, pds } via slingshot, fallback PLC + describeRepo | | loadHandle(did, opts?) | DID → handle (cached) | | loadHandles(dids, opts?) | parallel batch | | getPDS(did, opts?) | DID → PDS endpoint | | getPDSClient({ did }, opts?) | unauthenticated atcute Client | | listRecords({ did, collection, ... }) | repo records | | getRecord({ did, collection, rkey?, ... }) | single record | | getRecordByUri(uri, opts?) | record by AT URI (slingshot or fallback) | | describeRepo({ did, ... }) | repo metadata | | getBlobURL({ did, blob }) | direct PDS blob URL | | recentRecords(collection, opts?) | recent records by collection from UFO firehose | | countBacklinks(target, source, opts?) | constellation: like-count, follower-count, etc. | | countDistinctBacklinkers(target, source, opts?) | constellation: distinct-DID count | | listBacklinks(target, source, opts?) | constellation: paginated linking records | | listDistinctBacklinkers(target, source, opts?) | constellation: paginated distinct DIDs | | backlinksRollup(target, opts?) | constellation: all sources rolled up | | createTID() | TID rkey | | readThroughCache(cache, key, load) | the generic cache wrapper used internally |

Microcosm services (slingshot / UFO / constellation)

By default, three microcosm-rs services back the helpers (tangled mirror):

| Service | Default URL | Used by | |---|---|---| | Slingshot | slingshot.microcosm.blue | identity (handle ↔ DID ↔ PDS), getRecordByUri | | UFO | ufos-api.microcosm.blue | recentRecords (firehose by collection) | | Constellation | constellation.microcosm.blue | backlinks (likes, follows, replies, …) |

All three default URLs are swappable to a self-hosted instance per call:

loadHandle(did, { slingshot: 'https://my.host' });
recentRecords('xyz.statusphere.status', { ufo: 'https://my.host' });
countBacklinks(uri, source, { constellation: 'https://my.host' });

Slingshot 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.

All 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.

Constellation: backlinks

Given a target (AT URI or DID) and a { collection, path } source describing what links you're looking for, Constellation answers "who linked to this":

import { countBacklinks, listDistinctBacklinkers } from '@svelte-atproto/oauth/helper';

// like-count for a post
const likes = await countBacklinks(postUri, {
  collection: 'app.bsky.feed.like',
  path: '.subject.uri'
});

// follower-count for a DID
const followers = await countBacklinks(did, {
  collection: 'app.bsky.graph.follow',
  path: '.subject'
});

// who reposted a post
const page = await listDistinctBacklinkers(postUri, {
  collection: 'app.bsky.feed.repost',
  path: '.subject.uri'
}, { limit: 50 });
const dids = page?.dids ?? [];

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.

Browser-only flow (/browser)

For 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.

// src/lib/atproto.ts
import { createAtprotoBrowserAuth } from '@svelte-atproto/oauth/browser';

export const atproto = createAtprotoBrowserAuth({
	origin: 'https://my-app.example',
	scope: 'atproto',
	signupPDS: 'https://pds.rip/' // optional
});
// src/routes/oauth-client-metadata.json/+server.ts
import { atproto } from '$lib/atproto';
import { json } from '@sveltejs/kit';
export const prerender = true;
export const GET = () => json(atproto.metadata);
<!-- src/routes/+layout.svelte -->
<script>
	import { onMount } from 'svelte';
	import { atproto } from '$lib/atproto';
	onMount(() => atproto.init());
</script>

In components:

<script>
	import { atproto } from '$lib/atproto';
	const { user, login, logout } = atproto;
</script>

{#if $user.isInitializing}
	loading…
{:else if $user.isLoggedIn}
	signed in as {$user.did}
	<button onclick={logout}>Sign out</button>
{:else}
	<button onclick={() => login('alice.bsky.social')}>Sign in</button>
{/if}

In dev, the lib uses a loopback client_id automatically — no public URL or metadata route needed for local testing.

atproto.user is a svelte/store Readable so components consume it via $user.x (auto-subscription).

Bsky helpers (/bsky)

Opt-in. Anyone on a custom appview never imports this and pays nothing.

import { loadBskyProfile, loadBskyProfiles, getCDNImageBlobUrl } from '@svelte-atproto/oauth/bsky';

const profile = await loadBskyProfile(did, { cache });
const profiles = await loadBskyProfiles(dids, { cache });  // batched via app.bsky.actor.getProfiles (25/call)
const imgUrl = getCDNImageBlobUrl({ did, blob });

Stores

import { memory } from '@svelte-atproto/oauth/server/stores/memory';
import { cloudflareKV } from '@svelte-atproto/oauth/server/stores/cloudflare';
import { upstashRedis } from '@svelte-atproto/oauth/server/stores/upstash';

Or implement your own — anything matching atcute's Store<K, V> interface works.

cloudflareKV accepts either a binding name (looks it up via getRequestEvent().platform.env) or a KVNamespace directly.

CLI

atproto-oauth setup    # idempotent — generates COOKIE_SECRET + CLIENT_ASSERTION_KEY into .env
atproto-oauth secret   # print a fresh COOKIE_SECRET to stdout
atproto-oauth keygen   # print a fresh CLIENT_ASSERTION_KEY (JWK) to stdout

For production secrets, pipe into your secrets manager:

atproto-oauth secret | wrangler secret put COOKIE_SECRET
atproto-oauth keygen | wrangler secret put CLIENT_ASSERTION_KEY

Status

Pre-1.0. The public API is small and unlikely to churn, but expect breaking changes until stabilized. Issues + PRs welcome at https://github.com/flo-bit/svelte-atproto-oauth.

License

MIT