Sifa professional network API (Fastify, AT Protocol, Jetstream)
sifa.id/
1import { JoseKey, NodeOAuthClient } from '@atproto/oauth-client-node';
2import type { DidCache, HandleCache } from '@atproto/oauth-client';
3import type { Env } from '../config.js';
4import type { Database } from '../db/index.js';
5import { DbSessionStore } from './session-store.js';
6import { ValkeyStateStore, type ValkeyClient } from './state-store.js';
7import { ValkeyIdentityCache } from './identity-cache.js';
8import { loadPrivateKey } from './keys.js';
9
10/**
11 * Creates and configures the ATproto NodeOAuthClient with:
12 * - Database-backed session persistence (PostgreSQL via Drizzle)
13 * - Valkey-backed ephemeral state storage (10 min TTL)
14 * - Client metadata matching the /oauth/client-metadata.json endpoint
15 *
16 * The private key is loaded from disk (path derived from OAUTH_JWKS_PATH by
17 * replacing "jwks" with "private-key"). This key is used for private_key_jwt
18 * token endpoint authentication and DPoP proof signing.
19 *
20 * Async because JoseKey.fromJWK() needs to import the key material.
21 */
22export async function createOAuthClient(
23 config: Env,
24 db: Database,
25 valkey: ValkeyClient,
26): Promise<NodeOAuthClient> {
27 const privateKeyPath = config.OAUTH_JWKS_PATH.replace('jwks', 'private-key');
28 const privateKeyJwk = loadPrivateKey(privateKeyPath);
29 const key = await JoseKey.fromJWK(privateKeyJwk as Record<string, unknown>);
30
31 // Valkey-backed identity caches for the OAuth client.
32 // AT Protocol OAuth spec recommends <10 min cache for auth flows.
33 // Separate prefix from the shared resolver (which uses longer TTLs for non-auth paths).
34 // Fail-open: Valkey errors are treated as cache misses.
35 const didCache: DidCache = new ValkeyIdentityCache(valkey, 'oauth-did', 600); // 10 min TTL (spec)
36 const handleCache: HandleCache = new ValkeyIdentityCache(valkey, 'oauth-handle', 600); // 10 min TTL (spec)
37
38 return new NodeOAuthClient({
39 didCache,
40 handleCache,
41 clientMetadata: {
42 client_id: `${config.PUBLIC_URL}/oauth/client-metadata.json`,
43 client_name: 'Sifa',
44 client_uri: config.PUBLIC_URL,
45 response_types: ['code'],
46 grant_types: ['authorization_code', 'refresh_token'],
47 scope:
48 'atproto repo:id.sifa.profile.self repo:id.sifa.profile.position repo:id.sifa.profile.education repo:id.sifa.profile.skill repo:id.sifa.profile.externalAccount repo:id.sifa.graph.follow',
49 redirect_uris: [`${config.PUBLIC_URL}/oauth/callback`],
50 dpop_bound_access_tokens: true,
51 token_endpoint_auth_method: 'private_key_jwt',
52 token_endpoint_auth_signing_alg: 'ES256',
53 jwks_uri: `${config.PUBLIC_URL}/oauth/jwks.json`,
54 },
55 keyset: [key],
56 stateStore: new ValkeyStateStore(valkey),
57 sessionStore: new DbSessionStore(db),
58 });
59}