- Convert client-metadata.json to dynamic API route reading from env vars - Remove BLUESKY_CLIENT_ID and BLUESKY_REDIRECT_URI env vars - All OAuth URLs now derived from NEXT_PUBLIC_APP_URL - Implement production OAuth client (removes TODO/placeholder) - Update .prod.env with production settings for www.ponderants.com - Use https:// for production URLs - Simplify environment configuration (single source of truth) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
100 lines
3.2 KiB
TypeScript
100 lines
3.2 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 client metadata served from /client-metadata.json.
|
|
*
|
|
* 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 appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
|
|
const callbackUrl = `${appUrl}/api/auth/callback`;
|
|
|
|
if (isDev) {
|
|
// Development: Use localhost loopback client
|
|
// Per ATproto spec, we encode metadata in the client_id query params
|
|
// Request 'transition:generic' scope for repository write access
|
|
const clientId = `http://localhost/?${new URLSearchParams({
|
|
redirect_uri: callbackUrl,
|
|
scope: 'atproto transition:generic',
|
|
}).toString()}`;
|
|
|
|
console.log('[OAuth] Initializing development client with loopback exception');
|
|
console.log('[OAuth] client_id:', clientId);
|
|
|
|
clientInstance = new NodeOAuthClient({
|
|
clientMetadata: {
|
|
client_id: clientId,
|
|
redirect_uris: [callbackUrl],
|
|
scope: 'atproto transition:generic',
|
|
grant_types: ['authorization_code', 'refresh_token'],
|
|
response_types: ['code'],
|
|
application_type: 'native',
|
|
token_endpoint_auth_method: 'none',
|
|
dpop_bound_access_tokens: true,
|
|
},
|
|
stateStore: createStateStore(),
|
|
sessionStore: createSessionStore(),
|
|
});
|
|
|
|
console.log('[OAuth] ✓ Development client initialized');
|
|
} else {
|
|
// Production: Use client metadata from /client-metadata.json endpoint
|
|
const clientId = `${appUrl}/client-metadata.json`;
|
|
|
|
console.log('[OAuth] Initializing production client');
|
|
console.log('[OAuth] client_id:', clientId);
|
|
console.log('[OAuth] callback_url:', callbackUrl);
|
|
|
|
clientInstance = new NodeOAuthClient({
|
|
clientMetadata: {
|
|
client_id: clientId,
|
|
client_name: 'Ponderants',
|
|
client_uri: appUrl,
|
|
redirect_uris: [callbackUrl],
|
|
scope: 'atproto transition:generic',
|
|
grant_types: ['authorization_code', 'refresh_token'],
|
|
response_types: ['code'],
|
|
token_endpoint_auth_method: 'none',
|
|
application_type: 'web',
|
|
dpop_bound_access_tokens: true,
|
|
},
|
|
stateStore: createStateStore(),
|
|
sessionStore: createSessionStore(),
|
|
});
|
|
|
|
console.log('[OAuth] ✓ Production client initialized');
|
|
}
|
|
|
|
return clientInstance;
|
|
}
|
|
|
|
/**
|
|
* Clear the singleton instance (mainly for testing).
|
|
*/
|
|
export function clearOAuthClient(): void {
|
|
clientInstance = null;
|
|
}
|