feat: Make OAuth configuration environment-aware via NEXT_PUBLIC_APP_URL

- 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>
This commit is contained in:
2025-11-09 15:08:04 +00:00
parent 95eeef0deb
commit 5247c142a4
4 changed files with 66 additions and 37 deletions

View File

@@ -15,10 +15,10 @@ GOOGLE_AI_MODEL=gemini-pro-latest
# Deepgram API Key (for voice-to-text)
DEEPGRAM_API_KEY=your-deepgram-api-key
# Bluesky/ATproto OAuth Configuration (localhost development mode)
# See: https://atproto.com/specs/oauth#localhost-client-development
BLUESKY_CLIENT_ID=http://localhost/?redirect_uri=http://127.0.0.1:3000/api/auth/callback
BLUESKY_REDIRECT_URI=http://127.0.0.1:3000/api/auth/callback
# Application URL (used for OAuth callbacks and client metadata)
# In development, defaults to http://localhost:3000
# In production, set to your domain (e.g., https://www.ponderants.com)
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Test Account Credentials (for E2E tests)
TEST_BLUESKY_HANDLE=your-test-bluesky-handle

View File

@@ -0,0 +1,34 @@
import { NextResponse } from 'next/server';
/**
* ATproto OAuth Client Metadata Endpoint
*
* This endpoint serves the OAuth client metadata required for ATproto authentication.
* The client_id must match the URL where this metadata is served.
*
* @see https://atproto.com/specs/oauth
*/
export async function GET() {
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
const metadata = {
client_id: `${appUrl}/client-metadata.json`,
client_name: 'Ponderants',
client_uri: appUrl,
logo_uri: `${appUrl}/logo.svg`,
redirect_uris: [`${appUrl}/api/auth/callback`],
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,
};
return NextResponse.json(metadata, {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=3600', // Cache for 1 hour
},
});
}

View File

@@ -15,7 +15,7 @@ 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).
* In production, uses client metadata served from /client-metadata.json.
*
* The client handles:
* - OAuth authorization flow with PKCE
@@ -29,11 +29,8 @@ export async function getOAuthClient(): Promise<NodeOAuthClient> {
}
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');
}
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
const callbackUrl = `${appUrl}/api/auth/callback`;
if (isDev) {
// Development: Use localhost loopback client
@@ -64,13 +61,31 @@ export async function getOAuthClient(): Promise<NodeOAuthClient> {
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.'
);
// 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;

View File

@@ -1,20 +0,0 @@
{
"client_id": "https://www.ponderants.com/client-metadata.json",
"client_name": "Ponderants",
"client_uri": "https://www.ponderants.com",
"logo_uri": "https://www.ponderants.com/logo.svg",
"redirect_uris": [
"https://www.ponderants.com/api/auth/callback"
],
"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
}