- Fixed OAuth client configuration to properly use localhost for client_id and 127.0.0.1 for redirect_uris per RFC 8252 and ATproto spec - Added proper grapheme counting using RichText API instead of character length - Fixed thread splitting to account for link suffix and thread indicators in grapheme limits - Added GOOGLE_EMBEDDING_DIMENSIONS env var to all env files - Added clear-nodes.ts utility script for database management - Added galaxy node detail page route 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
106 lines
3.4 KiB
TypeScript
106 lines
3.4 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';
|
|
|
|
// Per RFC 8252 and ATproto OAuth spec:
|
|
// - client_id must use 'localhost' hostname (NOT an IP)
|
|
// - redirect_uris must use '127.0.0.1' loopback IP (NOT 'localhost')
|
|
const callbackUrl = isDev
|
|
? 'http://127.0.0.1:3000/api/auth/callback'
|
|
: `${appUrl}/api/auth/callback`;
|
|
|
|
if (isDev) {
|
|
// Development: Use localhost loopback client exception
|
|
// Encode metadata in client_id query params as per spec
|
|
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);
|
|
console.log('[OAuth] redirect_uri:', callbackUrl);
|
|
|
|
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;
|
|
}
|