+1
-1
netlify.toml
+1
-1
netlify.toml
+18
-76
netlify/functions/batch-follow-users.ts
+18
-76
netlify/functions/batch-follow-users.ts
···
1
1
import { Handler, HandlerEvent, HandlerResponse } from "@netlify/functions";
2
-
import {
3
-
NodeOAuthClient,
4
-
atprotoLoopbackClientMetadata,
5
-
} from "@atproto/oauth-client-node";
6
-
import { JoseKey } from "@atproto/jwk-jose";
7
-
import { stateStore, sessionStore, userSessions } from "./oauth-stores-db";
8
-
import { getOAuthConfig } from "./oauth-config";
9
-
import { Agent } from "@atproto/api";
2
+
import { SessionManager } from "./session-manager";
10
3
import cookie from "cookie";
11
-
12
-
function normalizePrivateKey(key: string): string {
13
-
if (!key.includes("\n") && key.includes("\\n")) {
14
-
return key.replace(/\\n/g, "\n");
15
-
}
16
-
return key;
17
-
}
18
4
19
5
export const handler: Handler = async (
20
6
event: HandlerEvent,
···
66
52
};
67
53
}
68
54
69
-
// Get DID from session
70
-
const userSession = await userSessions.get(sessionId);
71
-
if (!userSession) {
72
-
return {
73
-
statusCode: 401,
74
-
headers: { "Content-Type": "application/json" },
75
-
body: JSON.stringify({ error: "Invalid or expired session" }),
76
-
};
77
-
}
78
-
79
-
const config = getOAuthConfig();
80
-
const isDev = config.clientType === "loopback";
81
-
82
-
let client: NodeOAuthClient;
83
-
84
-
if (isDev) {
85
-
// Loopback
86
-
const clientMetadata = atprotoLoopbackClientMetadata(config.clientId);
87
-
client = new NodeOAuthClient({
88
-
clientMetadata: clientMetadata,
89
-
stateStore: stateStore as any,
90
-
sessionStore: sessionStore as any,
91
-
});
92
-
} else {
93
-
// Production with private key
94
-
const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY!);
95
-
const privateKey = await JoseKey.fromImportable(
96
-
normalizedKey,
97
-
"main-key",
98
-
);
99
-
100
-
client = new NodeOAuthClient({
101
-
clientMetadata: {
102
-
client_id: config.clientId,
103
-
client_name: "ATlast",
104
-
client_uri: config.clientId.replace(
105
-
"/oauth-client-metadata.json",
106
-
"",
107
-
),
108
-
redirect_uris: [config.redirectUri],
109
-
scope: "atproto transition:generic",
110
-
grant_types: ["authorization_code", "refresh_token"],
111
-
response_types: ["code"],
112
-
application_type: "web",
113
-
token_endpoint_auth_method: "private_key_jwt",
114
-
token_endpoint_auth_signing_alg: "ES256",
115
-
dpop_bound_access_tokens: true,
116
-
jwks_uri: config.jwksUri,
117
-
},
118
-
keyset: [privateKey],
119
-
stateStore: stateStore as any,
120
-
sessionStore: sessionStore as any,
121
-
});
122
-
}
123
-
124
-
// Restore OAuth session
125
-
const oauthSession = await client.restore(userSession.did);
126
-
127
-
// Create agent from OAuth session
128
-
const agent = new Agent(oauthSession);
55
+
// Get authenticated agent using SessionManager
56
+
const { agent, did: userDid } =
57
+
await SessionManager.getAgentForSession(sessionId);
129
58
130
59
// Follow all users
131
60
const results = [];
···
135
64
for (const did of dids) {
136
65
try {
137
66
await agent.api.com.atproto.repo.createRecord({
138
-
repo: userSession.did,
67
+
repo: userDid,
139
68
collection: "app.bsky.graph.follow",
140
69
record: {
141
70
$type: "app.bsky.graph.follow",
···
199
128
};
200
129
} catch (error) {
201
130
console.error("Batch follow error:", error);
131
+
132
+
// Handle authentication errors specifically
133
+
if (error instanceof Error && error.message.includes("session")) {
134
+
return {
135
+
statusCode: 401,
136
+
headers: { "Content-Type": "application/json" },
137
+
body: JSON.stringify({
138
+
error: "Invalid or expired session",
139
+
details: error.message,
140
+
}),
141
+
};
142
+
}
143
+
202
144
return {
203
145
statusCode: 500,
204
146
headers: { "Content-Type": "application/json" },
+16
-75
netlify/functions/batch-search-actors.ts
+16
-75
netlify/functions/batch-search-actors.ts
···
1
1
import { Handler, HandlerEvent, HandlerResponse } from "@netlify/functions";
2
-
import {
3
-
NodeOAuthClient,
4
-
atprotoLoopbackClientMetadata,
5
-
} from "@atproto/oauth-client-node";
6
-
import { JoseKey } from "@atproto/jwk-jose";
7
-
import { stateStore, sessionStore, userSessions } from "./oauth-stores-db";
8
-
import { getOAuthConfig } from "./oauth-config";
9
-
import { Agent } from "@atproto/api";
2
+
import { SessionManager } from "./session-manager";
10
3
import cookie from "cookie";
11
-
12
-
function normalizePrivateKey(key: string): string {
13
-
if (!key.includes("\n") && key.includes("\\n")) {
14
-
return key.replace(/\\n/g, "\n");
15
-
}
16
-
return key;
17
-
}
18
4
19
5
export const handler: Handler = async (
20
6
event: HandlerEvent,
···
57
43
};
58
44
}
59
45
60
-
// Get DID from session
61
-
const userSession = await userSessions.get(sessionId);
62
-
if (!userSession) {
63
-
return {
64
-
statusCode: 401,
65
-
headers: { "Content-Type": "application/json" },
66
-
body: JSON.stringify({ error: "Invalid or expired session" }),
67
-
};
68
-
}
69
-
70
-
const config = getOAuthConfig();
71
-
const isDev = config.clientType === "loopback";
72
-
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(
87
-
normalizedKey,
88
-
"main-key",
89
-
);
90
-
91
-
client = new NodeOAuthClient({
92
-
clientMetadata: {
93
-
client_id: config.clientId,
94
-
client_name: "ATlast",
95
-
client_uri: config.clientId.replace(
96
-
"/oauth-client-metadata.json",
97
-
"",
98
-
),
99
-
redirect_uris: [config.redirectUri],
100
-
scope: "atproto transition:generic",
101
-
grant_types: ["authorization_code", "refresh_token"],
102
-
response_types: ["code"],
103
-
application_type: "web",
104
-
token_endpoint_auth_method: "private_key_jwt",
105
-
token_endpoint_auth_signing_alg: "ES256",
106
-
dpop_bound_access_tokens: true,
107
-
jwks_uri: config.jwksUri,
108
-
},
109
-
keyset: [privateKey],
110
-
stateStore: stateStore as any,
111
-
sessionStore: sessionStore as any,
112
-
});
113
-
}
114
-
115
-
// Restore OAuth session
116
-
const oauthSession = await client.restore(userSession.did);
117
-
118
-
// Create agent from OAuth session
119
-
const agent = new Agent(oauthSession);
46
+
// Get authenticated agent using SessionManager
47
+
const { agent } = await SessionManager.getAgentForSession(sessionId);
120
48
121
49
// Search all usernames in parallel
122
50
const searchPromises = usernames.map(async (username) => {
···
230
158
};
231
159
} catch (error) {
232
160
console.error("Batch search error:", error);
161
+
162
+
// Handle authentication errors specifically
163
+
if (error instanceof Error && error.message.includes("session")) {
164
+
return {
165
+
statusCode: 401,
166
+
headers: { "Content-Type": "application/json" },
167
+
body: JSON.stringify({
168
+
error: "Invalid or expired session",
169
+
details: error.message,
170
+
}),
171
+
};
172
+
}
173
+
233
174
return {
234
175
statusCode: 500,
235
176
headers: { "Content-Type": "application/json" },
+65
netlify/functions/client.ts
+65
netlify/functions/client.ts
···
1
+
import {
2
+
NodeOAuthClient,
3
+
atprotoLoopbackClientMetadata,
4
+
} from "@atproto/oauth-client-node";
5
+
import { JoseKey } from "@atproto/jwk-jose";
6
+
import { stateStore, sessionStore } from "./oauth-stores-db";
7
+
import { getOAuthConfig } from "./oauth-config";
8
+
9
+
function normalizePrivateKey(key: string): string {
10
+
if (!key.includes("\n") && key.includes("\\n")) {
11
+
return key.replace(/\\n/g, "\n");
12
+
}
13
+
return key;
14
+
}
15
+
16
+
/**
17
+
* Creates and returns a configured OAuth client based on environment
18
+
* Centralizes the client creation logic used across all endpoints
19
+
*/
20
+
export async function createOAuthClient(): Promise<NodeOAuthClient> {
21
+
const config = getOAuthConfig();
22
+
const isDev = config.clientType === "loopback";
23
+
24
+
if (isDev) {
25
+
// Loopback mode for local development
26
+
console.log("[oauth-client] Creating loopback OAuth client");
27
+
const clientMetadata = atprotoLoopbackClientMetadata(config.clientId);
28
+
29
+
return new NodeOAuthClient({
30
+
clientMetadata: clientMetadata,
31
+
stateStore: stateStore as any,
32
+
sessionStore: sessionStore as any,
33
+
});
34
+
} else {
35
+
// Production mode with private key
36
+
console.log("[oauth-client] Creating production OAuth client");
37
+
38
+
if (!process.env.OAUTH_PRIVATE_KEY) {
39
+
throw new Error("OAUTH_PRIVATE_KEY is required for production");
40
+
}
41
+
42
+
const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY);
43
+
const privateKey = await JoseKey.fromImportable(normalizedKey, "main-key");
44
+
45
+
return new NodeOAuthClient({
46
+
clientMetadata: {
47
+
client_id: config.clientId,
48
+
client_name: "ATlast",
49
+
client_uri: config.clientId.replace("/oauth-client-metadata.json", ""),
50
+
redirect_uris: [config.redirectUri],
51
+
scope: "atproto transition:generic",
52
+
grant_types: ["authorization_code", "refresh_token"],
53
+
response_types: ["code"],
54
+
application_type: "web",
55
+
token_endpoint_auth_method: "private_key_jwt",
56
+
token_endpoint_auth_signing_alg: "ES256",
57
+
dpop_bound_access_tokens: true,
58
+
jwks_uri: config.jwksUri,
59
+
},
60
+
keyset: [privateKey],
61
+
stateStore: stateStore as any,
62
+
sessionStore: sessionStore as any,
63
+
});
64
+
}
65
+
}
+4
-8
netlify/functions/logout.ts
+4
-8
netlify/functions/logout.ts
···
1
1
import { Handler, HandlerEvent, HandlerResponse } from "@netlify/functions";
2
-
import { userSessions } from "./oauth-stores-db";
2
+
import { SessionManager } from "./session-manager";
3
3
import { getOAuthConfig } from "./oauth-config";
4
4
import cookie from "cookie";
5
5
···
27
27
console.log("[logout] Session ID from cookie:", sessionId);
28
28
29
29
if (sessionId) {
30
-
// Get the DID before deleting
31
-
const userSession = await userSessions.get(sessionId);
32
-
const did = userSession?.did;
33
-
34
-
// Delete session from database
35
-
await userSessions.del(sessionId);
36
-
console.log("[logout] Deleted session from database");
30
+
// Use SessionManager to properly clean up both user and OAuth sessions
31
+
await SessionManager.deleteSession(sessionId);
32
+
console.log("[logout] Successfully deleted session:", sessionId);
37
33
}
38
34
39
35
// Clear the session cookie with matching flags from when it was set
+17
-77
netlify/functions/oauth-callback.ts
+17
-77
netlify/functions/oauth-callback.ts
···
1
1
import { Handler, HandlerEvent, HandlerResponse } from "@netlify/functions";
2
-
import {
3
-
NodeOAuthClient,
4
-
atprotoLoopbackClientMetadata,
5
-
} from "@atproto/oauth-client-node";
6
-
import { JoseKey } from "@atproto/jwk-jose";
7
-
import { stateStore, sessionStore, userSessions } from "./oauth-stores-db";
2
+
import { createOAuthClient } from "./client";
3
+
import { userSessions } from "./oauth-stores-db";
8
4
import { getOAuthConfig } from "./oauth-config";
9
5
import * as crypto from "crypto";
10
6
11
-
function normalizePrivateKey(key: string): string {
12
-
if (!key.includes("\n") && key.includes("\\n")) {
13
-
return key.replace(/\\n/g, "\n");
14
-
}
15
-
return key;
16
-
}
17
-
18
7
export const handler: Handler = async (
19
8
event: HandlerEvent,
20
9
): Promise<HandlerResponse> => {
···
34
23
const code = params.get("code");
35
24
const state = params.get("state");
36
25
37
-
console.log("OAuth callback - Mode:", isDev ? "loopback" : "production");
38
-
console.log("OAuth callback - URL:", currentUrl);
26
+
console.log(
27
+
"[oauth-callback] Processing callback - Mode:",
28
+
isDev ? "loopback" : "production",
29
+
);
30
+
console.log("[oauth-callback] URL:", currentUrl);
39
31
40
32
if (!code || !state) {
41
33
return {
···
47
39
};
48
40
}
49
41
50
-
let client: NodeOAuthClient;
51
-
52
-
if (isDev) {
53
-
// LOOPBACK MODE: Use atprotoLoopbackClientMetadata and NO keyset
54
-
console.log("🔧 Loopback callback");
55
-
56
-
const clientMetadata = atprotoLoopbackClientMetadata(config.clientId);
57
-
58
-
client = new NodeOAuthClient({
59
-
clientMetadata: clientMetadata,
60
-
// No keyset for loopback!
61
-
stateStore: stateStore as any,
62
-
sessionStore: sessionStore as any,
63
-
});
64
-
} else {
65
-
// PRODUCTION MODE
66
-
if (!process.env.OAUTH_PRIVATE_KEY) {
67
-
console.error("OAUTH_PRIVATE_KEY not set");
68
-
return {
69
-
statusCode: 302,
70
-
headers: {
71
-
Location: `${currentUrl}/?error=Server configuration error`,
72
-
},
73
-
body: "",
74
-
};
75
-
}
76
-
77
-
const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY);
78
-
const privateKey = await JoseKey.fromImportable(
79
-
normalizedKey,
80
-
"main-key",
81
-
);
42
+
// Create OAuth client using shared helper
43
+
const client = await createOAuthClient();
82
44
83
-
const currentHost = process.env.DEPLOY_URL
84
-
? new URL(process.env.DEPLOY_URL).host
85
-
: event.headers["x-forwarded-host"] || event.headers.host;
45
+
// Process the OAuth callback
46
+
const result = await client.callback(params);
86
47
87
-
currentUrl = `https://${currentHost}`;
88
-
const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`;
89
-
const jwksUri = `${currentUrl}/.netlify/functions/jwks`;
90
-
const clientId = `${currentUrl}/oauth-client-metadata.json`;
91
-
92
-
client = new NodeOAuthClient({
93
-
clientMetadata: {
94
-
client_id: clientId,
95
-
client_name: "ATlast",
96
-
client_uri: currentUrl,
97
-
redirect_uris: [redirectUri],
98
-
scope: "atproto transition:generic",
99
-
grant_types: ["authorization_code", "refresh_token"],
100
-
response_types: ["code"],
101
-
application_type: "web",
102
-
token_endpoint_auth_method: "private_key_jwt",
103
-
token_endpoint_auth_signing_alg: "ES256",
104
-
dpop_bound_access_tokens: true,
105
-
jwks_uri: jwksUri,
106
-
} as any,
107
-
keyset: [privateKey],
108
-
stateStore: stateStore as any,
109
-
sessionStore: sessionStore as any,
110
-
});
111
-
}
112
-
113
-
const result = await client.callback(params);
48
+
console.log(
49
+
"[oauth-callback] Successfully authenticated DID:",
50
+
result.session.did,
51
+
);
114
52
115
53
// Store session
116
54
const sessionId = crypto.randomUUID();
117
55
const did = result.session.did;
118
56
await userSessions.set(sessionId, { did });
57
+
58
+
console.log("[oauth-callback] Created user session:", sessionId);
119
59
120
60
// Cookie flags - no Secure flag for loopback
121
61
const cookieFlags = isDev
+7
-77
netlify/functions/oauth-start.ts
+7
-77
netlify/functions/oauth-start.ts
···
1
1
import { Handler, HandlerEvent, HandlerResponse } from "@netlify/functions";
2
-
import {
3
-
NodeOAuthClient,
4
-
atprotoLoopbackClientMetadata,
5
-
} from "@atproto/oauth-client-node";
6
-
import { JoseKey } from "@atproto/jwk-jose";
7
-
import { stateStore, sessionStore } from "./oauth-stores-db";
8
-
import { getOAuthConfig } from "./oauth-config";
2
+
import { createOAuthClient } from "./client";
9
3
10
4
interface OAuthStartRequestBody {
11
5
login_hint?: string;
12
6
origin?: string;
13
7
}
14
8
15
-
function normalizePrivateKey(key: string): string {
16
-
if (!key.includes("\n") && key.includes("\\n")) {
17
-
return key.replace(/\\n/g, "\n");
18
-
}
19
-
return key;
20
-
}
21
-
22
9
export const handler: Handler = async (
23
10
event: HandlerEvent,
24
11
): Promise<HandlerResponse> => {
···
40
27
};
41
28
}
42
29
43
-
const config = getOAuthConfig();
44
-
const isDev = config.clientType === "loopback";
45
-
46
-
let client: NodeOAuthClient;
47
-
48
-
if (isDev) {
49
-
// LOOPBACK MODE: Use atprotoLoopbackClientMetadata and NO keyset
50
-
console.log("🔧 Using loopback OAuth client for development");
51
-
console.log("Client ID:", config.clientId);
52
-
53
-
const clientMetadata = atprotoLoopbackClientMetadata(config.clientId);
54
-
55
-
client = new NodeOAuthClient({
56
-
clientMetadata: clientMetadata,
57
-
stateStore: stateStore as any,
58
-
sessionStore: sessionStore as any,
59
-
});
60
-
} else {
61
-
// PRODUCTION MODE: Full confidential client with keyset
62
-
console.log("🔐 Using confidential OAuth client for production");
63
-
64
-
if (!process.env.OAUTH_PRIVATE_KEY) {
65
-
throw new Error("OAUTH_PRIVATE_KEY required for production");
66
-
}
67
-
68
-
const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY);
69
-
const privateKey = await JoseKey.fromImportable(
70
-
normalizedKey,
71
-
"main-key",
72
-
);
30
+
console.log("[oauth-start] Starting OAuth flow for:", loginHint);
73
31
74
-
const currentHost = process.env.DEPLOY_URL
75
-
? new URL(process.env.DEPLOY_URL).host
76
-
: event.headers["x-forwarded-host"] || event.headers.host;
77
-
78
-
if (!currentHost) {
79
-
throw new Error("Missing host header");
80
-
}
81
-
82
-
const currentUrl = `https://${currentHost}`;
83
-
const redirectUri = `${currentUrl}/.netlify/functions/oauth-callback`;
84
-
const jwksUri = `${currentUrl}/.netlify/functions/jwks`;
85
-
const clientId = `${currentUrl}/oauth-client-metadata.json`;
86
-
87
-
client = new NodeOAuthClient({
88
-
clientMetadata: {
89
-
client_id: clientId,
90
-
client_name: "ATlast",
91
-
client_uri: currentUrl,
92
-
redirect_uris: [redirectUri],
93
-
scope: "atproto transition:generic",
94
-
grant_types: ["authorization_code", "refresh_token"],
95
-
response_types: ["code"],
96
-
application_type: "web",
97
-
token_endpoint_auth_method: "private_key_jwt",
98
-
token_endpoint_auth_signing_alg: "ES256",
99
-
dpop_bound_access_tokens: true,
100
-
jwks_uri: jwksUri,
101
-
} as any,
102
-
keyset: [privateKey],
103
-
stateStore: stateStore as any,
104
-
sessionStore: sessionStore as any,
105
-
});
106
-
}
32
+
// Create OAuth client using shared helper
33
+
const client = await createOAuthClient();
107
34
35
+
// Start the authorization flow
108
36
const authUrl = await client.authorize(loginHint, {
109
37
scope: "atproto transition:generic",
110
38
});
39
+
40
+
console.log("[oauth-start] Generated auth URL for:", loginHint);
111
41
112
42
return {
113
43
statusCode: 200,
+92
netlify/functions/session-manager.ts
+92
netlify/functions/session-manager.ts
···
1
+
import { Agent } from "@atproto/api";
2
+
import { createOAuthClient } from "./client";
3
+
import { userSessions } from "./oauth-stores-db";
4
+
import type { NodeOAuthClient } from "@atproto/oauth-client-node";
5
+
6
+
/**
7
+
* Session Manager - Coordinates between user sessions and OAuth sessions
8
+
* Provides a clean interface for session operations across the application
9
+
*/
10
+
export class SessionManager {
11
+
/**
12
+
* Get an authenticated Agent for a given session ID
13
+
* Handles both user session lookup and OAuth session restoration
14
+
*/
15
+
static async getAgentForSession(sessionId: string): Promise<{
16
+
agent: Agent;
17
+
did: string;
18
+
client: NodeOAuthClient;
19
+
}> {
20
+
console.log("[SessionManager] Getting agent for session:", sessionId);
21
+
22
+
// Get user session
23
+
const userSession = await userSessions.get(sessionId);
24
+
if (!userSession) {
25
+
throw new Error("Invalid or expired session");
26
+
}
27
+
28
+
const did = userSession.did;
29
+
console.log("[SessionManager] Found user session for DID:", did);
30
+
31
+
// Create OAuth client
32
+
const client = await createOAuthClient();
33
+
34
+
// Restore OAuth session
35
+
const oauthSession = await client.restore(did);
36
+
console.log("[SessionManager] Restored OAuth session for DID:", did);
37
+
38
+
// Create agent from OAuth session
39
+
const agent = new Agent(oauthSession);
40
+
41
+
return { agent, did, client };
42
+
}
43
+
44
+
/**
45
+
* Delete a session and clean up associated OAuth sessions
46
+
* Ensures both user_sessions and oauth_sessions are cleaned up
47
+
*/
48
+
static async deleteSession(sessionId: string): Promise<void> {
49
+
console.log("[SessionManager] Deleting session:", sessionId);
50
+
51
+
// Get user session first
52
+
const userSession = await userSessions.get(sessionId);
53
+
if (!userSession) {
54
+
console.log("[SessionManager] Session not found:", sessionId);
55
+
return;
56
+
}
57
+
58
+
const did = userSession.did;
59
+
60
+
try {
61
+
// Create OAuth client and revoke the session
62
+
const client = await createOAuthClient();
63
+
64
+
// Try to revoke at the PDS (this also deletes from oauth_sessions)
65
+
await client.revoke(did);
66
+
console.log("[SessionManager] Revoked OAuth session for DID:", did);
67
+
} catch (error) {
68
+
// If revocation fails, the OAuth session might already be invalid
69
+
console.log("[SessionManager] Could not revoke OAuth session:", error);
70
+
}
71
+
72
+
// Delete user session
73
+
await userSessions.del(sessionId);
74
+
console.log("[SessionManager] Deleted user session:", sessionId);
75
+
}
76
+
77
+
/**
78
+
* Verify a session exists and is valid
79
+
*/
80
+
static async verifySession(sessionId: string): Promise<boolean> {
81
+
const userSession = await userSessions.get(sessionId);
82
+
return userSession !== null;
83
+
}
84
+
85
+
/**
86
+
* Get the DID for a session without creating an agent
87
+
*/
88
+
static async getDIDForSession(sessionId: string): Promise<string | null> {
89
+
const userSession = await userSessions.get(sessionId);
90
+
return userSession?.did || null;
91
+
}
92
+
}
+16
-72
netlify/functions/session.ts
+16
-72
netlify/functions/session.ts
···
1
1
import { Handler, HandlerEvent, HandlerResponse } from "@netlify/functions";
2
-
import {
3
-
NodeOAuthClient,
4
-
atprotoLoopbackClientMetadata,
5
-
} from "@atproto/oauth-client-node";
6
-
import { JoseKey } from "@atproto/jwk-jose";
7
-
import { stateStore, sessionStore, userSessions } from "./oauth-stores-db";
8
-
import { getOAuthConfig } from "./oauth-config";
9
-
import { Agent } from "@atproto/api";
2
+
import { SessionManager } from "./session-manager";
10
3
import cookie from "cookie";
11
-
12
-
function normalizePrivateKey(key: string): string {
13
-
if (!key.includes("\n") && key.includes("\\n")) {
14
-
return key.replace(/\\n/g, "\n");
15
-
}
16
-
return key;
17
-
}
18
4
19
5
// In-memory cache for profile
20
6
const profileCache = new Map<string, { data: any; timestamp: number }>();
···
38
24
};
39
25
}
40
26
41
-
// Check database for session
42
-
const userSession = await userSessions.get(sessionId);
43
-
44
-
if (!userSession) {
27
+
// Verify session exists
28
+
const isValid = await SessionManager.verifySession(sessionId);
29
+
if (!isValid) {
45
30
return {
46
31
statusCode: 401,
47
32
headers: { "Content-Type": "application/json" },
···
49
34
};
50
35
}
51
36
52
-
const did = userSession.did;
37
+
// Get DID from session
38
+
const did = await SessionManager.getDIDForSession(sessionId);
39
+
if (!did) {
40
+
return {
41
+
statusCode: 401,
42
+
headers: { "Content-Type": "application/json" },
43
+
body: JSON.stringify({ error: "Invalid session" }),
44
+
};
45
+
}
46
+
53
47
const now = Date.now();
54
48
55
49
// Check profile cache
···
71
65
72
66
// Cache miss - fetch full profile
73
67
try {
74
-
const config = getOAuthConfig();
75
-
const isDev = config.clientType === "loopback";
76
-
77
-
let client: NodeOAuthClient;
78
-
79
-
if (isDev) {
80
-
// Loopback
81
-
const clientMetadata = atprotoLoopbackClientMetadata(config.clientId);
82
-
client = new NodeOAuthClient({
83
-
clientMetadata: clientMetadata,
84
-
stateStore: stateStore as any,
85
-
sessionStore: sessionStore as any,
86
-
});
87
-
} else {
88
-
// Production with private key
89
-
const normalizedKey = normalizePrivateKey(
90
-
process.env.OAUTH_PRIVATE_KEY!,
91
-
);
92
-
const privateKey = await JoseKey.fromImportable(
93
-
normalizedKey,
94
-
"main-key",
95
-
);
96
-
97
-
client = new NodeOAuthClient({
98
-
clientMetadata: {
99
-
client_id: config.clientId,
100
-
client_name: "ATlast",
101
-
client_uri: config.clientId.replace(
102
-
"/oauth-client-metadata.json",
103
-
"",
104
-
),
105
-
redirect_uris: [config.redirectUri],
106
-
scope: "atproto transition:generic",
107
-
grant_types: ["authorization_code", "refresh_token"],
108
-
response_types: ["code"],
109
-
application_type: "web",
110
-
token_endpoint_auth_method: "private_key_jwt",
111
-
token_endpoint_auth_signing_alg: "ES256",
112
-
dpop_bound_access_tokens: true,
113
-
jwks_uri: config.jwksUri,
114
-
},
115
-
keyset: [privateKey],
116
-
stateStore: stateStore as any,
117
-
sessionStore: sessionStore as any,
118
-
});
119
-
}
120
-
121
-
// Restore OAuth session
122
-
const oauthSession = await client.restore(did);
123
-
124
-
// Create agent from OAuth session
125
-
const agent = new Agent(oauthSession);
68
+
// Get authenticated agent using SessionManager
69
+
const { agent } = await SessionManager.getAgentForSession(sessionId);
126
70
127
71
// Get profile
128
72
const profile = await agent.getProfile({ actor: did });