Replace manual OAuth implementation with official @atproto/oauth-client-node library to properly support DPoP (Demonstrating Proof of Possession) authentication. Changes: - Added @atproto/oauth-client-node dependency - Created OAuth state store (SurrealDB-backed) for CSRF protection - Created OAuth session store (SurrealDB-backed) for token persistence - Created OAuth client singleton with localhost exception for development - Rewrote /api/auth/login to use client.authorize() - Rewrote /api/auth/callback to use client.callback() with DPoP - Updated lib/auth/session.ts with getAuthenticatedAgent() for ATproto API calls - Updated db/schema.surql with oauth_state and oauth_session tables - Added scripts/apply-schema.js for database schema management - Created plans/oauth-dpop-implementation.md with detailed implementation plan - Removed legacy lib/auth/atproto.ts and lib/auth/oauth-state.ts - Updated .env to use localhost exception (removed BLUESKY_CLIENT_ID) The OAuth client now handles: - PKCE code generation and verification - DPoP proof generation and signing - Automatic token refresh - Session persistence across server restarts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
/**
|
|
* OAuth Client Singleton for ATproto
|
|
*
|
|
* This module provides a singleton instance of NodeOAuthClient
|
|
* that manages OAuth flows, DPoP proofs, and session persistence.
|
|
*/
|
|
|
|
import { NodeOAuthClient } from '@atproto/oauth-client-node';
|
|
import { createStateStore } from './oauth-state-store';
|
|
import { createSessionStore } from './oauth-session-store';
|
|
|
|
let clientInstance: NodeOAuthClient | null = null;
|
|
|
|
/**
|
|
* Get or create the singleton OAuth client instance.
|
|
*
|
|
* In development, uses the localhost client exception (no keys needed).
|
|
* In production, uses backend service with private keys (TODO).
|
|
*
|
|
* The client handles:
|
|
* - OAuth authorization flow with PKCE
|
|
* - DPoP (Demonstrating Proof of Possession) for token requests
|
|
* - Automatic token refresh
|
|
* - Session persistence in SurrealDB
|
|
*/
|
|
export async function getOAuthClient(): Promise<NodeOAuthClient> {
|
|
if (clientInstance) {
|
|
return clientInstance;
|
|
}
|
|
|
|
const isDev = process.env.NODE_ENV === 'development';
|
|
const callbackUrl = process.env.BLUESKY_REDIRECT_URI;
|
|
|
|
if (!callbackUrl) {
|
|
throw new Error('BLUESKY_REDIRECT_URI environment variable is required');
|
|
}
|
|
|
|
if (isDev) {
|
|
// Development: Use localhost exception
|
|
// Per ATproto spec, client_id must be exactly "http://localhost"
|
|
// (no port number) with metadata in query parameters
|
|
const clientId = `http://localhost?${new URLSearchParams({
|
|
redirect_uri: callbackUrl,
|
|
scope: 'atproto',
|
|
})}`;
|
|
|
|
console.log('[OAuth] Initializing development client with localhost exception');
|
|
console.log('[OAuth] client_id:', clientId);
|
|
|
|
clientInstance = await NodeOAuthClient.fromClientId({
|
|
clientId,
|
|
stateStore: createStateStore(),
|
|
sessionStore: createSessionStore(),
|
|
});
|
|
|
|
console.log('[OAuth] ✓ Development client initialized');
|
|
} else {
|
|
// Production: Backend service with keys
|
|
// TODO: Implement when deploying to production
|
|
// See plans/oauth-dpop-implementation.md for details
|
|
throw new Error(
|
|
'Production OAuth client not yet implemented. ' +
|
|
'See plans/oauth-dpop-implementation.md for production setup instructions.'
|
|
);
|
|
}
|
|
|
|
return clientInstance;
|
|
}
|
|
|
|
/**
|
|
* Clear the singleton instance (mainly for testing).
|
|
*/
|
|
export function clearOAuthClient(): void {
|
|
clientInstance = null;
|
|
}
|