+37
-23
netlify/functions/batch-follow-users.ts
+37
-23
netlify/functions/batch-follow-users.ts
···
1
1
import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions';
2
-
import { NodeOAuthClient } from '@atproto/oauth-client-node';
2
+
import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node';
3
3
import { JoseKey } from '@atproto/jwk-jose';
4
4
import { stateStore, sessionStore, userSessions } from './oauth-stores-db';
5
5
import { getOAuthConfig } from './oauth-config';
···
67
67
};
68
68
}
69
69
70
-
// Initialize OAuth client
71
70
const config = getOAuthConfig();
72
-
const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!);
73
-
const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key');
71
+
const isDev = config.clientType === 'loopback';
74
72
75
-
const client = new NodeOAuthClient({
76
-
clientMetadata: {
77
-
client_id: config.clientId,
78
-
client_name: 'ATlast',
79
-
client_uri: config.clientId.replace('/client-metadata.json', ''),
80
-
redirect_uris: [config.redirectUri],
81
-
scope: 'atproto transition:generic',
82
-
grant_types: ['authorization_code', 'refresh_token'],
83
-
response_types: ['code'],
84
-
application_type: 'web',
85
-
token_endpoint_auth_method: 'private_key_jwt',
86
-
token_endpoint_auth_signing_alg: 'ES256',
87
-
dpop_bound_access_tokens: true,
88
-
jwks_uri: config.jwksUri,
89
-
},
90
-
keyset: [privateKey],
91
-
stateStore: stateStore as any,
92
-
sessionStore: sessionStore as any,
93
-
});
73
+
let client: NodeOAuthClient;
74
+
75
+
if (isDev) {
76
+
// Loopback
77
+
const clientMetadata = atprotoLoopbackClientMetadata(config.clientId);
78
+
client = new NodeOAuthClient({
79
+
clientMetadata: clientMetadata,
80
+
stateStore: stateStore as any,
81
+
sessionStore: sessionStore as any,
82
+
});
83
+
} else {
84
+
// Production with private key
85
+
const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!);
86
+
const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key');
87
+
88
+
client = new NodeOAuthClient({
89
+
clientMetadata: {
90
+
client_id: config.clientId,
91
+
client_name: 'ATlast',
92
+
client_uri: config.clientId.replace('/client-metadata.json', ''),
93
+
redirect_uris: [config.redirectUri],
94
+
scope: 'atproto transition:generic',
95
+
grant_types: ['authorization_code', 'refresh_token'],
96
+
response_types: ['code'],
97
+
application_type: 'web',
98
+
token_endpoint_auth_method: 'private_key_jwt',
99
+
token_endpoint_auth_signing_alg: 'ES256',
100
+
dpop_bound_access_tokens: true,
101
+
jwks_uri: config.jwksUri,
102
+
},
103
+
keyset: [privateKey],
104
+
stateStore: stateStore as any,
105
+
sessionStore: sessionStore as any,
106
+
});
107
+
}
94
108
95
109
// Restore OAuth session
96
110
const oauthSession = await client.restore(userSession.did);
+37
-23
netlify/functions/batch-search-actors.ts
+37
-23
netlify/functions/batch-search-actors.ts
···
1
1
import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions';
2
-
import { NodeOAuthClient } from '@atproto/oauth-client-node';
2
+
import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node';
3
3
import { JoseKey } from '@atproto/jwk-jose';
4
4
import { stateStore, sessionStore, userSessions } from './oauth-stores-db';
5
5
import { getOAuthConfig } from './oauth-config';
···
58
58
};
59
59
}
60
60
61
-
// Initialize OAuth client
62
61
const config = getOAuthConfig();
63
-
const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!);
64
-
const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key');
62
+
const isDev = config.clientType === 'loopback';
65
63
66
-
const client = new NodeOAuthClient({
67
-
clientMetadata: {
68
-
client_id: config.clientId,
69
-
client_name: 'ATlast',
70
-
client_uri: config.clientId.replace('/client-metadata.json', ''),
71
-
redirect_uris: [config.redirectUri],
72
-
scope: 'atproto transition:generic',
73
-
grant_types: ['authorization_code', 'refresh_token'],
74
-
response_types: ['code'],
75
-
application_type: 'web',
76
-
token_endpoint_auth_method: 'private_key_jwt',
77
-
token_endpoint_auth_signing_alg: 'ES256',
78
-
dpop_bound_access_tokens: true,
79
-
jwks_uri: config.jwksUri,
80
-
},
81
-
keyset: [privateKey],
82
-
stateStore: stateStore as any,
83
-
sessionStore: sessionStore as any,
84
-
});
64
+
let client: NodeOAuthClient;
65
+
66
+
if (isDev) {
67
+
// Loopback
68
+
const clientMetadata = atprotoLoopbackClientMetadata(config.clientId);
69
+
client = new NodeOAuthClient({
70
+
clientMetadata: clientMetadata,
71
+
stateStore: stateStore as any,
72
+
sessionStore: sessionStore as any,
73
+
});
74
+
} else {
75
+
// Production with private key
76
+
const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!);
77
+
const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key');
78
+
79
+
client = new NodeOAuthClient({
80
+
clientMetadata: {
81
+
client_id: config.clientId,
82
+
client_name: 'ATlast',
83
+
client_uri: config.clientId.replace('/client-metadata.json', ''),
84
+
redirect_uris: [config.redirectUri],
85
+
scope: 'atproto transition:generic',
86
+
grant_types: ['authorization_code', 'refresh_token'],
87
+
response_types: ['code'],
88
+
application_type: 'web',
89
+
token_endpoint_auth_method: 'private_key_jwt',
90
+
token_endpoint_auth_signing_alg: 'ES256',
91
+
dpop_bound_access_tokens: true,
92
+
jwks_uri: config.jwksUri,
93
+
},
94
+
keyset: [privateKey],
95
+
stateStore: stateStore as any,
96
+
sessionStore: sessionStore as any,
97
+
});
98
+
}
85
99
86
100
// Restore OAuth session
87
101
const oauthSession = await client.restore(userSession.did);
+36
-12
netlify/functions/client-metadata.ts
+36
-12
netlify/functions/client-metadata.ts
···
1
1
import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions';
2
2
3
3
export const handler: Handler = async (event: HandlerEvent): Promise<HandlerResponse> => {
4
+
4
5
try {
5
6
// Get the host that's requesting the metadata
6
7
// This will be different for production vs preview deploys vs dev --live
···
16
17
};
17
18
}
18
19
19
-
// Build the redirect URI based on the requesting host
20
+
// Check if this is a loopback/development request
21
+
const isLoopback = requestHost.startsWith('127.0.0.1') ||
22
+
requestHost.startsWith('[::1]') ||
23
+
requestHost === 'localhost';
24
+
25
+
if (isLoopback) {
26
+
// For loopback clients, return minimal metadata
27
+
// NOTE: In practice, the OAuth server won't fetch this because
28
+
// loopback clients use hardcoded metadata on the server side
29
+
const appUrl = `http://${requestHost}`;
30
+
const redirectUri = `${appUrl}/.netlify/functions/oauth-callback`;
31
+
32
+
return {
33
+
statusCode: 200,
34
+
headers: {
35
+
'Content-Type': 'application/json',
36
+
'Access-Control-Allow-Origin': '*',
37
+
},
38
+
body: JSON.stringify({
39
+
client_id: appUrl, // Just the origin for loopback
40
+
client_name: 'ATlast (Local Dev)',
41
+
client_uri: appUrl,
42
+
redirect_uris: [redirectUri],
43
+
scope: 'atproto transition:generic',
44
+
grant_types: ['authorization_code', 'refresh_token'],
45
+
response_types: ['code'],
46
+
application_type: 'web',
47
+
token_endpoint_auth_method: 'none', // No auth for loopback
48
+
dpop_bound_access_tokens: true,
49
+
}),
50
+
};
51
+
}
52
+
53
+
// Production: Confidential client metadata
20
54
const redirectUri = `https://${requestHost}/.netlify/functions/oauth-callback`;
21
55
const appUrl = `https://${requestHost}`;
22
56
const jwksUri = `https://${requestHost}/.netlify/functions/jwks`;
23
57
const clientId = `https://${requestHost}/.netlify/functions/client-metadata`;
24
-
25
-
console.log('Client metadata generated for host:', {
26
-
requestHost,
27
-
redirectUri,
28
-
appUrl,
29
-
clientId,
30
-
jwksUri,
31
-
});
32
58
33
59
const metadata = {
34
60
client_id: clientId,
···
50
76
headers: {
51
77
'Content-Type': 'application/json',
52
78
'Access-Control-Allow-Origin': '*',
53
-
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
54
-
'Pragma': 'no-cache',
55
-
'Expires': '0',
79
+
'Cache-Control': 'no-store'
56
80
},
57
81
body: JSON.stringify(metadata),
58
82
};
+70
-58
netlify/functions/oauth-callback.ts
+70
-58
netlify/functions/oauth-callback.ts
···
1
1
import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions';
2
-
import { NodeOAuthClient } from '@atproto/oauth-client-node';
2
+
import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node';
3
3
import { JoseKey } from '@atproto/jwk-jose';
4
4
import { stateStore, sessionStore, userSessions } from './oauth-stores-db';
5
5
import { getOAuthConfig } from './oauth-config';
···
13
13
}
14
14
15
15
export const handler: Handler = async (event: HandlerEvent): Promise<HandlerResponse> => {
16
-
let currentHost = process.env.DEPLOY_URL
17
-
? new URL(process.env.DEPLOY_URL).host
18
-
: (event.headers['x-forwarded-host'] || event.headers.host);
19
-
let currentUrl = currentHost ? `https://${currentHost}` : process.env.URL || process.env.DEPLOY_PRIME_URL || 'https://atlast.byarielm.fyi';
20
-
const fallbackUrl = currentUrl;
16
+
const config = getOAuthConfig();
17
+
const isDev = config.clientType === 'loopback';
18
+
19
+
let currentUrl = isDev
20
+
? 'http://127.0.0.1:8888'
21
+
: (process.env.DEPLOY_URL
22
+
? `https://${new URL(process.env.DEPLOY_URL).host}`
23
+
: process.env.URL || process.env.DEPLOY_PRIME_URL || 'https://atlast.byarielm.fyi');
21
24
22
25
try {
23
26
const params = new URLSearchParams(event.rawUrl.split('?')[1] || '');
24
27
const code = params.get('code');
25
28
const state = params.get('state');
26
29
27
-
console.log('OAuth callback - Host:', currentHost);
28
-
console.log('OAuth callback - currentUrl resolved to:', currentUrl);
30
+
console.log('OAuth callback - Mode:', isDev ? 'loopback' : 'production');
31
+
console.log('OAuth callback - URL:', currentUrl);
29
32
30
33
if (!code || !state) {
31
34
return {
···
37
40
};
38
41
}
39
42
40
-
if (!process.env.OAUTH_PRIVATE_KEY) {
41
-
console.error('OAUTH_PRIVATE_KEY not set');
42
-
return {
43
-
statusCode: 302,
44
-
headers: {
45
-
'Location': `${currentUrl}/?error=Server configuration error`
46
-
},
47
-
body: ''
48
-
};
49
-
}
50
-
51
-
const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY);
52
-
const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key');
53
-
54
-
// All URIs must now be based on the current deploy URL/host
55
-
const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`;
56
-
const jwksUri = `${currentUrl}/.netlify/functions/jwks`; // NOW DYNAMIC URL
57
-
const clientId = `${currentUrl}/.netlify/functions/client-metadata`; // NOW DYNAMIC URL
43
+
let client: NodeOAuthClient;
58
44
59
-
console.log('OAuth callback URLs:', {
60
-
redirectUri,
61
-
jwksUri,
62
-
clientId,
63
-
currentUrl
64
-
});
45
+
if (isDev) {
46
+
// LOOPBACK MODE: Use atprotoLoopbackClientMetadata and NO keyset
47
+
console.log('🔧 Loopback callback');
48
+
49
+
const clientMetadata = atprotoLoopbackClientMetadata(config.clientId);
50
+
51
+
client = new NodeOAuthClient({
52
+
clientMetadata: clientMetadata,
53
+
// No keyset for loopback!
54
+
stateStore: stateStore as any,
55
+
sessionStore: sessionStore as any,
56
+
});
57
+
} else {
58
+
// PRODUCTION MODE
59
+
if (!process.env.OAUTH_PRIVATE_KEY) {
60
+
console.error('OAUTH_PRIVATE_KEY not set');
61
+
return {
62
+
statusCode: 302,
63
+
headers: { 'Location': `${currentUrl}/?error=Server configuration error` },
64
+
body: ''
65
+
};
66
+
}
65
67
66
-
// Build metadata dynamically based on the current environment
67
-
const clientMetadata = {
68
-
client_id: clientId, // NOW DYNAMIC URL
69
-
client_name: 'ATlast',
70
-
client_uri: currentUrl,
71
-
redirect_uris: [redirectUri],
72
-
scope: 'atproto transition:generic',
73
-
grant_types: ['authorization_code', 'refresh_token'],
74
-
response_types: ['code'],
75
-
application_type: 'web',
76
-
token_endpoint_auth_method: 'private_key_jwt',
77
-
token_endpoint_auth_signing_alg: 'ES256',
78
-
dpop_bound_access_tokens: true,
79
-
jwks_uri: jwksUri, // NOW DYNAMIC URL
80
-
};
68
+
const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY);
69
+
const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key');
70
+
71
+
const currentHost = process.env.DEPLOY_URL
72
+
? new URL(process.env.DEPLOY_URL).host
73
+
: (event.headers['x-forwarded-host'] || event.headers.host);
74
+
75
+
currentUrl = `https://${currentHost}`;
76
+
const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`;
77
+
const jwksUri = `${currentUrl}/.netlify/functions/jwks`;
78
+
const clientId = `${currentUrl}/.netlify/functions/client-metadata`;
81
79
82
-
// Initialize OAuth client with dynamic metadata
83
-
const client = new NodeOAuthClient({
84
-
clientMetadata: clientMetadata as any,
85
-
keyset: [privateKey],
86
-
stateStore: stateStore as any,
87
-
sessionStore: sessionStore as any,
88
-
});
80
+
client = new NodeOAuthClient({
81
+
clientMetadata: {
82
+
client_id: clientId,
83
+
client_name: 'ATlast',
84
+
client_uri: currentUrl,
85
+
redirect_uris: [redirectUri],
86
+
scope: 'atproto transition:generic',
87
+
grant_types: ['authorization_code', 'refresh_token'],
88
+
response_types: ['code'],
89
+
application_type: 'web',
90
+
token_endpoint_auth_method: 'private_key_jwt',
91
+
token_endpoint_auth_signing_alg: 'ES256',
92
+
dpop_bound_access_tokens: true,
93
+
jwks_uri: jwksUri,
94
+
} as any,
95
+
keyset: [privateKey],
96
+
stateStore: stateStore as any,
97
+
sessionStore: sessionStore as any,
98
+
});
99
+
}
89
100
90
101
const result = await client.callback(params);
91
102
92
-
// Store a simple session mapping: sessionId -> DID
103
+
// Store session
93
104
const sessionId = crypto.randomUUID();
94
105
const did = result.session.did;
95
-
96
106
await userSessions.set(sessionId, { did });
97
107
98
-
const cookieFlags = 'HttpOnly; SameSite=Lax; Max-Age=1209600; Path=/; Secure';
108
+
// Cookie flags - no Secure flag for loopback
109
+
const cookieFlags = isDev
110
+
? 'HttpOnly; SameSite=Lax; Max-Age=1209600; Path=/'
111
+
: 'HttpOnly; SameSite=Lax; Max-Age=1209600; Path=/; Secure';
99
112
100
113
return {
101
114
statusCode: 302,
···
108
121
109
122
} catch (error) {
110
123
console.error('OAuth callback error:', error);
111
-
112
124
return {
113
125
statusCode: 302,
114
126
headers: {
+23
-5
netlify/functions/oauth-config.ts
+23
-5
netlify/functions/oauth-config.ts
···
1
1
export function getOAuthConfig() {
2
-
// In Netlify, process.env.URL is automatically set to the public URL.
2
+
// Development: loopback client for local dev
3
+
const isDev = process.env.NODE_ENV === 'development' || process.env.NETLIFY_DEV === 'true';
4
+
5
+
if (isDev) {
6
+
const port = process.env.PORT || '8888';
7
+
8
+
// Special loopback client_id format with query params
9
+
const clientId = `http://localhost?${new URLSearchParams([
10
+
['redirect_uri', `http://127.0.0.1:${port}/.netlify/functions/oauth-callback`],
11
+
['scope', 'atproto transition:generic'],
12
+
])}`;
13
+
14
+
return {
15
+
clientId: clientId,
16
+
redirectUri: `http://127.0.0.1:${port}/.netlify/functions/oauth-callback`,
17
+
jwksUri: undefined,
18
+
clientType: 'loopback' as const,
19
+
};
20
+
}
21
+
22
+
// Production: discoverable client logic
3
23
const baseUrl = process.env.URL || process.env.DEPLOY_PRIME_URL;
4
24
5
25
if (process.env.NETLIFY && !process.env.URL) {
6
-
// This is a safety check for a critical configuration issue on Netlify
7
26
throw new Error('process.env.URL is required in Netlify environment');
8
27
}
9
28
···
13
32
CONTEXT: process.env.CONTEXT,
14
33
using: baseUrl
15
34
});
16
-
17
-
const redirectUri = `${baseUrl}/.netlify/functions/oauth-callback`;
18
35
19
36
return {
20
37
clientId: `${baseUrl}/.netlify/functions/client-metadata`, // discoverable client URL
21
-
redirectUri,
38
+
redirectUri: `${baseUrl}/.netlify/functions/oauth-callback`,
22
39
jwksUri: `${baseUrl}/.netlify/functions/jwks`,
23
40
clientType: 'discoverable' as const,
41
+
usePrivateKey: true,
24
42
};
25
43
}
+61
-77
netlify/functions/oauth-start.ts
+61
-77
netlify/functions/oauth-start.ts
···
1
1
import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions';
2
-
import { NodeOAuthClient } from '@atproto/oauth-client-node';
2
+
import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node';
3
3
import { JoseKey } from '@atproto/jwk-jose';
4
4
import { stateStore, sessionStore } from './oauth-stores-db';
5
5
import { getOAuthConfig } from './oauth-config';
6
-
import { initDB } from './db'; // initDB is only kept for manual setup/migrations
7
6
8
7
interface OAuthStartRequestBody {
9
8
login_hint?: string;
10
-
origin?: string; // The actual origin the frontend is running on
9
+
origin?: string;
11
10
}
12
11
13
12
function normalizePrivateKey(key: string): string {
···
19
18
20
19
export const handler: Handler = async (event: HandlerEvent): Promise<HandlerResponse> => {
21
20
try {
22
-
// await initDB();
23
-
24
-
// Parse request body
25
21
let loginHint: string | undefined = undefined;
26
-
let requestOrigin: string | undefined = undefined;
27
22
28
23
if (event.body) {
29
24
const parsed: OAuthStartRequestBody = JSON.parse(event.body);
30
25
loginHint = parsed.login_hint;
31
-
requestOrigin = parsed.origin;
32
26
}
33
27
34
-
// Validate login hint is provided
35
28
if (!loginHint) {
36
29
return {
37
30
statusCode: 400,
···
39
32
body: JSON.stringify({ error: 'login_hint (handle or DID) is required' }),
40
33
};
41
34
}
42
-
43
-
console.log('OAuth Start - Request origin:', requestOrigin);
44
35
45
-
// Validate private key
46
-
if (!process.env.OAUTH_PRIVATE_KEY) {
47
-
console.error('OAUTH_PRIVATE_KEY not set');
48
-
return {
49
-
statusCode: 500,
50
-
headers: { 'Content-Type': 'application/json' },
51
-
body: JSON.stringify({ error: 'Server configuration error' }),
52
-
};
53
-
}
36
+
const config = getOAuthConfig();
37
+
const isDev = config.clientType === 'loopback';
54
38
55
-
// Initialize OAuth client
56
-
const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY);
57
-
const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key');
39
+
let client: NodeOAuthClient;
58
40
59
-
// Use the dynamic config that Netlify sets for the build
60
-
const currentHost = process.env.DEPLOY_URL
61
-
? new URL(process.env.DEPLOY_URL).host
62
-
: (event.headers['x-forwarded-host'] || event.headers.host);
63
-
64
-
if (!currentHost) {
65
-
console.error('Missing host header in function request');
66
-
return {
67
-
statusCode: 500,
68
-
headers: { 'Content-Type': 'application/json' },
69
-
body: JSON.stringify({ error: 'Server could not determine current host for redirect' }),
70
-
};
71
-
}
72
-
73
-
const currentUrl = `https://${currentHost}`;
41
+
if (isDev) {
42
+
// LOOPBACK MODE: Use atprotoLoopbackClientMetadata and NO keyset
43
+
console.log('🔧 Using loopback OAuth client for development');
44
+
console.log('Client ID:', config.clientId);
45
+
46
+
const clientMetadata = atprotoLoopbackClientMetadata(config.clientId);
47
+
48
+
client = new NodeOAuthClient({
49
+
clientMetadata: clientMetadata,
50
+
stateStore: stateStore as any,
51
+
sessionStore: sessionStore as any,
52
+
});
53
+
} else {
54
+
// PRODUCTION MODE: Full confidential client with keyset
55
+
console.log('🔐 Using confidential OAuth client for production');
56
+
57
+
if (!process.env.OAUTH_PRIVATE_KEY) {
58
+
throw new Error('OAUTH_PRIVATE_KEY required for production');
59
+
}
74
60
75
-
// Now, dynamically define the URIs using the CURRENT HOST
76
-
const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`;
77
-
const appUrl = currentUrl;
78
-
const jwksUri = `${currentUrl}/.netlify/functions/jwks`;
79
-
const clientId = `${currentUrl}/.netlify/functions/client-metadata`;
80
-
81
-
console.log('OAuth URLs:', {
82
-
redirectUri,
83
-
appUrl,
84
-
jwksUri,
85
-
clientId,
86
-
requestOrigin
87
-
});
61
+
const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY);
62
+
const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key');
88
63
89
-
// Build metadata dynamically from environment
90
-
const clientMetadata = {
91
-
client_id: clientId,
92
-
client_name: 'ATlast',
93
-
client_uri: appUrl,
94
-
redirect_uris: [redirectUri],
95
-
scope: 'atproto transition:generic',
96
-
grant_types: ['authorization_code', 'refresh_token'],
97
-
response_types: ['code'],
98
-
application_type: 'web',
99
-
token_endpoint_auth_method: 'private_key_jwt',
100
-
token_endpoint_auth_signing_alg: 'ES256',
101
-
dpop_bound_access_tokens: true,
102
-
jwks_uri: jwksUri,
103
-
};
64
+
const currentHost = process.env.DEPLOY_URL
65
+
? new URL(process.env.DEPLOY_URL).host
66
+
: (event.headers['x-forwarded-host'] || event.headers.host);
104
67
105
-
console.log('Client metadata:', clientMetadata);
68
+
if (!currentHost) {
69
+
throw new Error('Missing host header');
70
+
}
106
71
107
-
// Initialize NodeOAuthClient with typed stores
108
-
const client = new NodeOAuthClient({
109
-
clientMetadata: clientMetadata as any,
110
-
keyset: [privateKey],
111
-
stateStore: stateStore as any,
112
-
sessionStore: sessionStore as any,
113
-
});
72
+
const currentUrl = `https://${currentHost}`;
73
+
const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`;
74
+
const jwksUri = `${currentUrl}/.netlify/functions/jwks`;
75
+
const clientId = `${currentUrl}/.netlify/functions/client-metadata`;
114
76
115
-
console.log('OAuth client initialized with redirect_uri:', redirectUri);
77
+
client = new NodeOAuthClient({
78
+
clientMetadata: {
79
+
client_id: clientId,
80
+
client_name: 'ATlast',
81
+
client_uri: currentUrl,
82
+
redirect_uris: [redirectUri],
83
+
scope: 'atproto transition:generic',
84
+
grant_types: ['authorization_code', 'refresh_token'],
85
+
response_types: ['code'],
86
+
application_type: 'web',
87
+
token_endpoint_auth_method: 'private_key_jwt',
88
+
token_endpoint_auth_signing_alg: 'ES256',
89
+
dpop_bound_access_tokens: true,
90
+
jwks_uri: jwksUri,
91
+
} as any,
92
+
keyset: [privateKey],
93
+
stateStore: stateStore as any,
94
+
sessionStore: sessionStore as any,
95
+
});
96
+
}
116
97
117
-
// Generate authorization URL
118
98
const authUrl = await client.authorize(loginHint, {
119
99
scope: 'atproto transition:generic',
120
100
});
···
129
109
return {
130
110
statusCode: 500,
131
111
headers: { 'Content-Type': 'application/json' },
132
-
body: JSON.stringify({ error: 'Failed to start OAuth flow' }),
112
+
body: JSON.stringify({
113
+
error: 'Failed to start OAuth flow',
114
+
details: error instanceof Error ? error.message : 'Unknown error',
115
+
stack: error instanceof Error ? error.stack : undefined
116
+
}),
133
117
};
134
118
}
135
119
};
+37
-22
netlify/functions/session.ts
+37
-22
netlify/functions/session.ts
···
1
1
import { Handler, HandlerEvent, HandlerResponse } from '@netlify/functions';
2
-
import { NodeOAuthClient } from '@atproto/oauth-client-node';
2
+
import { NodeOAuthClient, atprotoLoopbackClientMetadata } from '@atproto/oauth-client-node';
3
3
import { JoseKey } from '@atproto/jwk-jose';
4
4
import { stateStore, sessionStore, userSessions } from './oauth-stores-db';
5
5
import { getOAuthConfig } from './oauth-config';
···
106
106
// Cache miss - fetch full profile
107
107
try {
108
108
const config = getOAuthConfig();
109
-
const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!);
110
-
const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key');
109
+
const isDev = config.clientType === 'loopback';
110
+
111
+
let client: NodeOAuthClient;
112
+
113
+
if (isDev) {
114
+
// Loopback
115
+
const clientMetadata = atprotoLoopbackClientMetadata(config.clientId);
116
+
client = new NodeOAuthClient({
117
+
clientMetadata: clientMetadata,
118
+
stateStore: stateStore as any,
119
+
sessionStore: sessionStore as any,
120
+
});
121
+
} else {
122
+
// Production with private key
123
+
const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!);
124
+
const privateKey = await JoseKey.fromImportable(normalizedKey, 'main-key');
111
125
112
-
const client = new NodeOAuthClient({
113
-
clientMetadata: {
114
-
client_id: config.clientId,
115
-
client_name: 'ATlast',
116
-
client_uri: config.clientId.replace('/client-metadata.json', ''),
117
-
redirect_uris: [config.redirectUri],
118
-
scope: 'atproto transition:generic',
119
-
grant_types: ['authorization_code', 'refresh_token'],
120
-
response_types: ['code'],
121
-
application_type: 'web',
122
-
token_endpoint_auth_method: 'private_key_jwt',
123
-
token_endpoint_auth_signing_alg: 'ES256',
124
-
dpop_bound_access_tokens: true,
125
-
jwks_uri: config.jwksUri,
126
-
},
127
-
keyset: [privateKey],
128
-
stateStore: stateStore as any,
129
-
sessionStore: sessionStore as any,
130
-
});
126
+
client = new NodeOAuthClient({
127
+
clientMetadata: {
128
+
client_id: config.clientId,
129
+
client_name: 'ATlast',
130
+
client_uri: config.clientId.replace('/client-metadata.json', ''),
131
+
redirect_uris: [config.redirectUri],
132
+
scope: 'atproto transition:generic',
133
+
grant_types: ['authorization_code', 'refresh_token'],
134
+
response_types: ['code'],
135
+
application_type: 'web',
136
+
token_endpoint_auth_method: 'private_key_jwt',
137
+
token_endpoint_auth_signing_alg: 'ES256',
138
+
dpop_bound_access_tokens: true,
139
+
jwks_uri: config.jwksUri,
140
+
},
141
+
keyset: [privateKey],
142
+
stateStore: stateStore as any,
143
+
sessionStore: sessionStore as any,
144
+
});
145
+
}
131
146
132
147
// Restore OAuth session
133
148
const oauthSession = await client.restore(did);