Fixed multiple issues with the @atproto/oauth-client-node integration:
1. OAuth State Store:
- Changed from SQL WHERE queries to SurrealDB record IDs
- Use `oauth_state:⟨${key}⟩` pattern for direct lookups
- Fixes "Parse error: Unexpected token" issues
2. OAuth Session Store:
- Changed from SQL WHERE queries to SurrealDB record IDs
- Use `oauth_session:⟨${did}⟩` pattern for direct lookups
- Implement proper upsert logic with select + merge/create
3. OAuth Client Configuration:
- Use loopback pattern with metadata in client_id query params
- Format: `http://localhost/?redirect_uri=...&scope=atproto`
- Complies with ATproto OAuth localhost development mode
4. Auth Callback:
- Remove getProfile API call that requires additional scopes
- Use DID directly from session for user identification
- Simplify user creation in SurrealDB with record IDs
5. Login Page:
- Change from GET redirect to POST with JSON body
- Properly handle errors and display to user
The OAuth flow now works end-to-end:
- User enters handle → redirects to Bluesky OAuth
- User authorizes → callback exchanges code for tokens
- Session stored in SurrealDB → user redirected to /chat
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
84 lines
2.5 KiB
TypeScript
84 lines
2.5 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 loopback client
|
|
// Per ATproto spec, we encode metadata in the client_id query params
|
|
const clientId = `http://localhost/?${new URLSearchParams({
|
|
redirect_uri: callbackUrl,
|
|
scope: 'atproto',
|
|
}).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',
|
|
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: 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;
|
|
}
|