@sigma-auth/better-auth-plugin 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +5 -3
- package/dist/client/index.js.map +1 -1
- package/dist/provider/index.d.ts +6 -0
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +555 -522
- package/dist/provider/index.js.map +1 -1
- package/package.json +1 -1
package/dist/provider/index.js
CHANGED
|
@@ -4,6 +4,23 @@ import { setSessionCookie } from "better-auth/cookies";
|
|
|
4
4
|
import { createAuthMiddleware } from "better-auth/plugins";
|
|
5
5
|
import { parseAuthToken, verifyAuthToken } from "bitcoin-auth";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
+
function createDebugLogger(enabled) {
|
|
8
|
+
const noop = () => { };
|
|
9
|
+
if (!enabled) {
|
|
10
|
+
return { log: noop, warn: noop, error: noop };
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
log: (message, ...args) => {
|
|
14
|
+
console.log(`[Sigma Debug] ${message}`, ...args);
|
|
15
|
+
},
|
|
16
|
+
warn: (message, ...args) => {
|
|
17
|
+
console.warn(`[Sigma Debug] ${message}`, ...args);
|
|
18
|
+
},
|
|
19
|
+
error: (message, ...args) => {
|
|
20
|
+
console.error(`[Sigma Debug] ${message}`, ...args);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
7
24
|
/**
|
|
8
25
|
* Sigma Auth provider plugin for Better Auth
|
|
9
26
|
* This is the OAuth provider that runs on auth.sigmaidentity.com
|
|
@@ -26,576 +43,592 @@ import { z } from "zod";
|
|
|
26
43
|
* });
|
|
27
44
|
* ```
|
|
28
45
|
*/
|
|
29
|
-
export const sigmaProvider = (options) =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
export const sigmaProvider = (options) => {
|
|
47
|
+
const debug = createDebugLogger(options?.debug ?? false);
|
|
48
|
+
return {
|
|
49
|
+
id: "sigma",
|
|
50
|
+
schema: {
|
|
51
|
+
user: {
|
|
52
|
+
fields: {
|
|
53
|
+
pubkey: {
|
|
54
|
+
type: "string",
|
|
55
|
+
required: true,
|
|
56
|
+
unique: true,
|
|
57
|
+
},
|
|
58
|
+
...(options?.enableSubscription
|
|
59
|
+
? {
|
|
60
|
+
subscriptionTier: {
|
|
61
|
+
type: "string",
|
|
62
|
+
required: false,
|
|
63
|
+
defaultValue: "free",
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
: {}),
|
|
38
67
|
},
|
|
39
|
-
...(options?.enableSubscription
|
|
40
|
-
? {
|
|
41
|
-
subscriptionTier: {
|
|
42
|
-
type: "string",
|
|
43
|
-
required: false,
|
|
44
|
-
defaultValue: "free",
|
|
45
|
-
},
|
|
46
|
-
}
|
|
47
|
-
: {}),
|
|
48
68
|
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
session: {
|
|
70
|
+
fields: {
|
|
71
|
+
...(options?.enableSubscription
|
|
72
|
+
? {
|
|
73
|
+
subscriptionTier: {
|
|
74
|
+
type: "string",
|
|
75
|
+
required: false,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
79
|
+
},
|
|
60
80
|
},
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
81
|
+
oauthAccessToken: {
|
|
82
|
+
fields: {
|
|
83
|
+
selectedBapId: {
|
|
84
|
+
type: "string",
|
|
85
|
+
required: false,
|
|
86
|
+
},
|
|
67
87
|
},
|
|
68
88
|
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
oauthApplication: {
|
|
90
|
+
fields: {
|
|
91
|
+
owner_bap_id: {
|
|
92
|
+
type: "string",
|
|
93
|
+
required: true,
|
|
94
|
+
},
|
|
75
95
|
},
|
|
76
96
|
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
97
|
+
oauthConsent: {
|
|
98
|
+
fields: {
|
|
99
|
+
selectedBapId: {
|
|
100
|
+
type: "string",
|
|
101
|
+
required: false,
|
|
102
|
+
},
|
|
83
103
|
},
|
|
84
104
|
},
|
|
85
105
|
},
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (grantType !== "authorization_code") {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
// Check if token exchange was successful
|
|
100
|
-
const responseBody = ctx.context.returned;
|
|
101
|
-
if (!responseBody ||
|
|
102
|
-
typeof responseBody !== "object" ||
|
|
103
|
-
!("access_token" in responseBody)) {
|
|
104
|
-
return; // Token exchange failed, skip BAP ID storage
|
|
105
|
-
}
|
|
106
|
-
// Only proceed if we have cache option
|
|
107
|
-
if (!options?.cache) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
try {
|
|
111
|
-
// Get the access token from response to find the related consent
|
|
112
|
-
const accessToken = responseBody
|
|
113
|
-
.access_token;
|
|
114
|
-
// Query the access token record to get userId and clientId using adapter
|
|
115
|
-
const tokenRecords = await ctx.context.adapter.findMany({
|
|
116
|
-
model: "oauthAccessToken",
|
|
117
|
-
where: [{ field: "accessToken", value: accessToken }],
|
|
118
|
-
limit: 1,
|
|
119
|
-
});
|
|
120
|
-
if (tokenRecords.length === 0 || !tokenRecords[0]) {
|
|
121
|
-
console.warn("⚠️ [OAuth Token Hook] No access token found in database");
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
const { userId, clientId } = tokenRecords[0];
|
|
125
|
-
// Query the most recent consent record for this user/client to get selectedBapId
|
|
126
|
-
const consentRecords = await ctx.context.adapter.findMany({
|
|
127
|
-
model: "oauthConsent",
|
|
128
|
-
where: [
|
|
129
|
-
{ field: "userId", value: userId },
|
|
130
|
-
{ field: "clientId", value: clientId },
|
|
131
|
-
],
|
|
132
|
-
limit: 1,
|
|
133
|
-
sortBy: { field: "createdAt", direction: "desc" },
|
|
134
|
-
});
|
|
135
|
-
const consentRecord = consentRecords[0];
|
|
136
|
-
if (!consentRecord || !consentRecord.selectedBapId) {
|
|
106
|
+
hooks: {
|
|
107
|
+
after: [
|
|
108
|
+
{
|
|
109
|
+
matcher: (ctx) => ctx.path === "/oauth2/token",
|
|
110
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
111
|
+
debug.log("AFTER hook triggered for /oauth2/token");
|
|
112
|
+
const body = ctx.body;
|
|
113
|
+
const grantType = body.grant_type;
|
|
114
|
+
// Only handle authorization_code grant (not refresh_token)
|
|
115
|
+
if (grantType !== "authorization_code") {
|
|
137
116
|
return;
|
|
138
117
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
selectedBapId,
|
|
146
|
-
},
|
|
147
|
-
});
|
|
148
|
-
console.log(`✅ [OAuth Token Hook] Stored BAP ID in access token: user=${userId.substring(0, 15)}... bap=${selectedBapId.substring(0, 15)}...`);
|
|
149
|
-
// Update user record with selected identity's profile data
|
|
150
|
-
if (options?.getPool) {
|
|
151
|
-
const pool = options.getPool();
|
|
152
|
-
// Use pool.query() directly to avoid connect/release pattern
|
|
153
|
-
// This prevents "Pool release event triggered outside of request scope" warning
|
|
154
|
-
const profileResult = await pool.query("SELECT bap_id, name, image, member_pubkey FROM profile WHERE bap_id = $1 AND user_id = $2 LIMIT 1", [selectedBapId, userId]);
|
|
155
|
-
const profile = profileResult.rows[0];
|
|
156
|
-
if (profile) {
|
|
157
|
-
// Update user record with profile data
|
|
158
|
-
await ctx.context.adapter.update({
|
|
159
|
-
model: "user",
|
|
160
|
-
where: [{ field: "id", value: userId }],
|
|
161
|
-
update: {
|
|
162
|
-
name: profile.name,
|
|
163
|
-
image: profile.image,
|
|
164
|
-
...(profile.member_pubkey && {
|
|
165
|
-
pubkey: profile.member_pubkey,
|
|
166
|
-
}),
|
|
167
|
-
updatedAt: new Date(),
|
|
168
|
-
},
|
|
169
|
-
});
|
|
170
|
-
}
|
|
118
|
+
// Check if token exchange was successful
|
|
119
|
+
const responseBody = ctx.context.returned;
|
|
120
|
+
if (!responseBody ||
|
|
121
|
+
typeof responseBody !== "object" ||
|
|
122
|
+
!("access_token" in responseBody)) {
|
|
123
|
+
return; // Token exchange failed, skip BAP ID storage
|
|
171
124
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
console.error("❌ [OAuth Token] Error storing identity selection:", error);
|
|
175
|
-
}
|
|
176
|
-
}),
|
|
177
|
-
},
|
|
178
|
-
// NOTE: Userinfo enrichment is handled by getAdditionalUserInfoClaim in auth server config
|
|
179
|
-
// This avoids duplicate database queries and pool release warnings
|
|
180
|
-
// The auth server's getAdditionalUserInfoClaim looks up selectedBapId and fetches BAP profile
|
|
181
|
-
{
|
|
182
|
-
matcher: (ctx) => ctx.path === "/oauth2/consent",
|
|
183
|
-
handler: createAuthMiddleware(async (ctx) => {
|
|
184
|
-
// Only proceed if we have cache option
|
|
185
|
-
if (!options?.cache) {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
const body = ctx.body;
|
|
189
|
-
const consentCode = body.consent_code;
|
|
190
|
-
const accept = body.accept;
|
|
191
|
-
// Only store selectedBapId if consent was accepted
|
|
192
|
-
if (!accept || !consentCode) {
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
try {
|
|
196
|
-
// Get session for userId
|
|
197
|
-
const session = ctx.context.session;
|
|
198
|
-
if (!session?.user?.id) {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
// Wait a bit for Better Auth to create the consent record
|
|
202
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
203
|
-
// Query the database to get the clientId from the consent record
|
|
204
|
-
const consentRecords = await ctx.context.adapter.findMany({
|
|
205
|
-
model: "oauthConsent",
|
|
206
|
-
where: [{ field: "userId", value: session.user.id }],
|
|
207
|
-
limit: 1,
|
|
208
|
-
sortBy: { field: "createdAt", direction: "desc" },
|
|
209
|
-
});
|
|
210
|
-
const consentRecord = consentRecords[0];
|
|
211
|
-
if (!consentRecord) {
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
const { id: consentId } = consentRecord;
|
|
215
|
-
// Retrieve selected BAP ID from cache/KV
|
|
216
|
-
const selectedBapId = await options.cache.get(`consent:${consentCode}:bap_id`);
|
|
217
|
-
if (!selectedBapId) {
|
|
125
|
+
// Only proceed if we have cache option
|
|
126
|
+
if (!options?.cache) {
|
|
218
127
|
return;
|
|
219
128
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
console.error("❌ [OAuth Consent Hook] Error storing identity selection:", error);
|
|
232
|
-
}
|
|
233
|
-
}),
|
|
234
|
-
},
|
|
235
|
-
],
|
|
236
|
-
before: [
|
|
237
|
-
{
|
|
238
|
-
matcher: (ctx) => ctx.path === "/oauth2/token",
|
|
239
|
-
handler: createAuthMiddleware(async (ctx) => {
|
|
240
|
-
const body = ctx.body;
|
|
241
|
-
const grantType = body.grant_type;
|
|
242
|
-
// Handle authorization_code grant type (exchange code for token)
|
|
243
|
-
if (grantType === "authorization_code") {
|
|
244
|
-
// Get client_id from request body
|
|
245
|
-
const clientId = body.client_id;
|
|
246
|
-
if (!clientId) {
|
|
247
|
-
throw new APIError("BAD_REQUEST", {
|
|
248
|
-
message: "Missing client_id in request body",
|
|
129
|
+
try {
|
|
130
|
+
// Get the access token from response to find the related consent
|
|
131
|
+
const accessToken = responseBody
|
|
132
|
+
.access_token;
|
|
133
|
+
// Query the access token record to get userId and clientId using adapter
|
|
134
|
+
const tokenRecords = await ctx.context.adapter.findMany({
|
|
135
|
+
model: "oauthAccessToken",
|
|
136
|
+
where: [{ field: "accessToken", value: accessToken }],
|
|
137
|
+
limit: 1,
|
|
249
138
|
});
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const authToken = headers.get("x-auth-token");
|
|
265
|
-
if (!authToken) {
|
|
266
|
-
throw new APIError("UNAUTHORIZED", {
|
|
267
|
-
message: "Missing X-Auth-Token header for client authentication",
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
// Parse the auth token to extract pubkey
|
|
271
|
-
const parsed = parseAuthToken(authToken);
|
|
272
|
-
if (!parsed?.pubkey) {
|
|
273
|
-
throw new APIError("UNAUTHORIZED", {
|
|
274
|
-
message: "Invalid Bitcoin auth token format",
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
// Verify the pubkey from signature matches the client's memberPubkey
|
|
278
|
-
if (!client.metadata) {
|
|
279
|
-
throw new APIError("UNAUTHORIZED", {
|
|
280
|
-
message: `Client ${clientId} has no metadata`,
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
const metadata = JSON.parse(client.metadata);
|
|
284
|
-
const expectedPubkey = metadata.memberPubkey;
|
|
285
|
-
if (!expectedPubkey) {
|
|
286
|
-
throw new APIError("UNAUTHORIZED", {
|
|
287
|
-
message: `Client ${clientId} has no memberPubkey in metadata`,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
if (parsed.pubkey !== expectedPubkey) {
|
|
291
|
-
throw new APIError("UNAUTHORIZED", {
|
|
292
|
-
message: "Bitcoin signature pubkey does not match client",
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
// Get request body for signature verification
|
|
296
|
-
const bodyString = new URLSearchParams(Object.entries(body).map(([k, v]) => [k, String(v)])).toString();
|
|
297
|
-
// Verify Bitcoin signature with body
|
|
298
|
-
const verifyData = {
|
|
299
|
-
requestPath: "/oauth2/token",
|
|
300
|
-
timestamp: parsed.timestamp,
|
|
301
|
-
body: bodyString,
|
|
302
|
-
};
|
|
303
|
-
const isValid = verifyAuthToken(authToken, verifyData, 5);
|
|
304
|
-
if (!isValid) {
|
|
305
|
-
throw new APIError("UNAUTHORIZED", {
|
|
306
|
-
message: "Invalid Bitcoin signature",
|
|
139
|
+
if (tokenRecords.length === 0 || !tokenRecords[0]) {
|
|
140
|
+
debug.warn("No access token found in database");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const { userId, clientId } = tokenRecords[0];
|
|
144
|
+
// Query the most recent consent record for this user/client to get selectedBapId
|
|
145
|
+
const consentRecords = await ctx.context.adapter.findMany({
|
|
146
|
+
model: "oauthConsent",
|
|
147
|
+
where: [
|
|
148
|
+
{ field: "userId", value: userId },
|
|
149
|
+
{ field: "clientId", value: clientId },
|
|
150
|
+
],
|
|
151
|
+
limit: 1,
|
|
152
|
+
sortBy: { field: "createdAt", direction: "desc" },
|
|
307
153
|
});
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
// Handle refresh_token grant type
|
|
323
|
-
if (grantType === "refresh_token") {
|
|
324
|
-
const refreshToken = body.refresh_token;
|
|
325
|
-
if (!refreshToken) {
|
|
326
|
-
throw new APIError("BAD_REQUEST", {
|
|
327
|
-
message: "Missing refresh_token",
|
|
154
|
+
const consentRecord = consentRecords[0];
|
|
155
|
+
if (!consentRecord || !consentRecord.selectedBapId) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const selectedBapId = consentRecord.selectedBapId;
|
|
159
|
+
// Update the oauthAccessToken record with the selected BAP ID
|
|
160
|
+
await ctx.context.adapter.update({
|
|
161
|
+
model: "oauthAccessToken",
|
|
162
|
+
where: [{ field: "accessToken", value: accessToken }],
|
|
163
|
+
update: {
|
|
164
|
+
selectedBapId,
|
|
165
|
+
},
|
|
328
166
|
});
|
|
167
|
+
debug.log(`Stored BAP ID in access token: user=${userId.substring(0, 15)}... bap=${selectedBapId.substring(0, 15)}...`);
|
|
168
|
+
// Update user record with selected identity's profile data
|
|
169
|
+
if (options?.getPool) {
|
|
170
|
+
const pool = options.getPool();
|
|
171
|
+
// Use pool.query() directly to avoid connect/release pattern
|
|
172
|
+
// This prevents "Pool release event triggered outside of request scope" warning
|
|
173
|
+
const profileResult = await pool.query("SELECT bap_id, name, image, member_pubkey FROM profile WHERE bap_id = $1 AND user_id = $2 LIMIT 1", [selectedBapId, userId]);
|
|
174
|
+
const profile = profileResult.rows[0];
|
|
175
|
+
if (profile) {
|
|
176
|
+
// Update user record with profile data
|
|
177
|
+
await ctx.context.adapter.update({
|
|
178
|
+
model: "user",
|
|
179
|
+
where: [{ field: "id", value: userId }],
|
|
180
|
+
update: {
|
|
181
|
+
name: profile.name,
|
|
182
|
+
image: profile.image,
|
|
183
|
+
...(profile.member_pubkey && {
|
|
184
|
+
pubkey: profile.member_pubkey,
|
|
185
|
+
}),
|
|
186
|
+
updatedAt: new Date(),
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
329
191
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (!clientId) {
|
|
333
|
-
throw new APIError("BAD_REQUEST", {
|
|
334
|
-
message: "Missing client_id in request body",
|
|
335
|
-
});
|
|
192
|
+
catch (error) {
|
|
193
|
+
debug.error("Error storing identity selection:", error);
|
|
336
194
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
195
|
+
}),
|
|
196
|
+
},
|
|
197
|
+
// NOTE: Userinfo enrichment is handled by getAdditionalUserInfoClaim in auth server config
|
|
198
|
+
// This avoids duplicate database queries and pool release warnings
|
|
199
|
+
// The auth server's getAdditionalUserInfoClaim looks up selectedBapId and fetches BAP profile
|
|
200
|
+
{
|
|
201
|
+
matcher: (ctx) => ctx.path === "/oauth2/consent",
|
|
202
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
203
|
+
// Only proceed if we have cache option
|
|
204
|
+
if (!options?.cache) {
|
|
205
|
+
return;
|
|
346
206
|
}
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
if (!
|
|
352
|
-
|
|
353
|
-
message: "Missing X-Auth-Token header for client authentication",
|
|
354
|
-
});
|
|
207
|
+
const body = ctx.body;
|
|
208
|
+
const consentCode = body.consent_code;
|
|
209
|
+
const accept = body.accept;
|
|
210
|
+
// Only store selectedBapId if consent was accepted
|
|
211
|
+
if (!accept || !consentCode) {
|
|
212
|
+
return;
|
|
355
213
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
214
|
+
try {
|
|
215
|
+
// Get session for userId
|
|
216
|
+
const session = ctx.context.session;
|
|
217
|
+
if (!session?.user?.id) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// Wait a bit for Better Auth to create the consent record
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
222
|
+
// Query the database to get the clientId from the consent record
|
|
223
|
+
const consentRecords = await ctx.context.adapter.findMany({
|
|
224
|
+
model: "oauthConsent",
|
|
225
|
+
where: [{ field: "userId", value: session.user.id }],
|
|
226
|
+
limit: 1,
|
|
227
|
+
sortBy: { field: "createdAt", direction: "desc" },
|
|
360
228
|
});
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
229
|
+
const consentRecord = consentRecords[0];
|
|
230
|
+
if (!consentRecord) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const { id: consentId } = consentRecord;
|
|
234
|
+
// Retrieve selected BAP ID from cache/KV
|
|
235
|
+
const selectedBapId = await options.cache.get(`consent:${consentCode}:bap_id`);
|
|
236
|
+
if (!selectedBapId) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Update the consent record with selectedBapId using adapter
|
|
240
|
+
await ctx.context.adapter.update({
|
|
241
|
+
model: "oauthConsent",
|
|
242
|
+
where: [{ field: "id", value: consentId }],
|
|
243
|
+
update: {
|
|
244
|
+
selectedBapId,
|
|
245
|
+
},
|
|
365
246
|
});
|
|
247
|
+
debug.log(`Stored BAP ID in consent: user=${session.user.id.substring(0, 15)}... bap=${selectedBapId.substring(0, 15)}...`);
|
|
366
248
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (!expectedPubkey) {
|
|
370
|
-
throw new APIError("UNAUTHORIZED", {
|
|
371
|
-
message: `Client ${clientId} has no memberPubkey in metadata`,
|
|
372
|
-
});
|
|
249
|
+
catch (error) {
|
|
250
|
+
debug.error("Error storing consent identity selection:", error);
|
|
373
251
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
252
|
+
}),
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
before: [
|
|
256
|
+
{
|
|
257
|
+
matcher: (ctx) => ctx.path === "/oauth2/token",
|
|
258
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
259
|
+
const body = ctx.body;
|
|
260
|
+
const grantType = body.grant_type;
|
|
261
|
+
// Handle authorization_code grant type (exchange code for token)
|
|
262
|
+
if (grantType === "authorization_code") {
|
|
263
|
+
// Get client_id from request body
|
|
264
|
+
const clientId = body.client_id;
|
|
265
|
+
if (!clientId) {
|
|
266
|
+
throw new APIError("BAD_REQUEST", {
|
|
267
|
+
message: "Missing client_id in request body",
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// Lookup OAuth client by client_id
|
|
271
|
+
const clients = await ctx.context.adapter.findMany({
|
|
272
|
+
model: "oauthApplication",
|
|
273
|
+
where: [{ field: "clientId", value: clientId }],
|
|
377
274
|
});
|
|
275
|
+
if (clients.length === 0) {
|
|
276
|
+
throw new APIError("UNAUTHORIZED", {
|
|
277
|
+
message: `OAuth client not registered: ${clientId}`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const client = clients[0];
|
|
281
|
+
// Validate client authentication via Bitcoin signature
|
|
282
|
+
const headers = new Headers(ctx.headers || {});
|
|
283
|
+
const authToken = headers.get("x-auth-token");
|
|
284
|
+
if (!authToken) {
|
|
285
|
+
throw new APIError("UNAUTHORIZED", {
|
|
286
|
+
message: "Missing X-Auth-Token header for client authentication",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
// Parse the auth token to extract pubkey
|
|
290
|
+
const parsed = parseAuthToken(authToken);
|
|
291
|
+
if (!parsed?.pubkey) {
|
|
292
|
+
throw new APIError("UNAUTHORIZED", {
|
|
293
|
+
message: "Invalid Bitcoin auth token format",
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
// Verify the pubkey from signature matches the client's memberPubkey
|
|
297
|
+
if (!client.metadata) {
|
|
298
|
+
throw new APIError("UNAUTHORIZED", {
|
|
299
|
+
message: `Client ${clientId} has no metadata`,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const metadata = JSON.parse(client.metadata);
|
|
303
|
+
const expectedPubkey = metadata.memberPubkey;
|
|
304
|
+
if (!expectedPubkey) {
|
|
305
|
+
throw new APIError("UNAUTHORIZED", {
|
|
306
|
+
message: `Client ${clientId} has no memberPubkey in metadata`,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (parsed.pubkey !== expectedPubkey) {
|
|
310
|
+
throw new APIError("UNAUTHORIZED", {
|
|
311
|
+
message: "Bitcoin signature pubkey does not match client",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
// Get request body for signature verification
|
|
315
|
+
const bodyString = new URLSearchParams(Object.entries(body).map(([k, v]) => [k, String(v)])).toString();
|
|
316
|
+
// Verify Bitcoin signature with body
|
|
317
|
+
const verifyData = {
|
|
318
|
+
requestPath: "/oauth2/token",
|
|
319
|
+
timestamp: parsed.timestamp,
|
|
320
|
+
body: bodyString,
|
|
321
|
+
};
|
|
322
|
+
const isValid = verifyAuthToken(authToken, verifyData, 5);
|
|
323
|
+
if (!isValid) {
|
|
324
|
+
throw new APIError("UNAUTHORIZED", {
|
|
325
|
+
message: "Invalid Bitcoin signature",
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
debug.log(`Client authenticated via Bitcoin signature (clientId: ${clientId})`);
|
|
329
|
+
// Inject client_id into request body for Better Auth to process
|
|
330
|
+
const modifiedBody = {
|
|
331
|
+
...ctx.body,
|
|
332
|
+
client_id: clientId,
|
|
333
|
+
};
|
|
334
|
+
return {
|
|
335
|
+
context: {
|
|
336
|
+
...ctx,
|
|
337
|
+
body: modifiedBody,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
378
340
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
341
|
+
// Handle refresh_token grant type
|
|
342
|
+
if (grantType === "refresh_token") {
|
|
343
|
+
const refreshToken = body.refresh_token;
|
|
344
|
+
if (!refreshToken) {
|
|
345
|
+
throw new APIError("BAD_REQUEST", {
|
|
346
|
+
message: "Missing refresh_token",
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
// Get client_id from request body
|
|
350
|
+
const clientId = body.client_id;
|
|
351
|
+
if (!clientId) {
|
|
352
|
+
throw new APIError("BAD_REQUEST", {
|
|
353
|
+
message: "Missing client_id in request body",
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
// Lookup OAuth client by client_id
|
|
357
|
+
const clients = await ctx.context.adapter.findMany({
|
|
358
|
+
model: "oauthApplication",
|
|
359
|
+
where: [{ field: "clientId", value: clientId }],
|
|
389
360
|
});
|
|
361
|
+
if (clients.length === 0) {
|
|
362
|
+
throw new APIError("UNAUTHORIZED", {
|
|
363
|
+
message: `OAuth client not registered: ${clientId}`,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
const client = clients[0];
|
|
367
|
+
// Validate client signature first
|
|
368
|
+
const headers = new Headers(ctx.headers || {});
|
|
369
|
+
const authToken = headers.get("x-auth-token");
|
|
370
|
+
if (!authToken) {
|
|
371
|
+
throw new APIError("UNAUTHORIZED", {
|
|
372
|
+
message: "Missing X-Auth-Token header for client authentication",
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
const parsed = parseAuthToken(authToken);
|
|
376
|
+
if (!parsed?.pubkey) {
|
|
377
|
+
throw new APIError("UNAUTHORIZED", {
|
|
378
|
+
message: "Invalid Bitcoin auth token format",
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
if (!client.metadata) {
|
|
382
|
+
throw new APIError("UNAUTHORIZED", {
|
|
383
|
+
message: `Client ${clientId} has no metadata`,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
const metadata = JSON.parse(client.metadata);
|
|
387
|
+
const expectedPubkey = metadata.memberPubkey;
|
|
388
|
+
if (!expectedPubkey) {
|
|
389
|
+
throw new APIError("UNAUTHORIZED", {
|
|
390
|
+
message: `Client ${clientId} has no memberPubkey in metadata`,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
if (parsed.pubkey !== expectedPubkey) {
|
|
394
|
+
throw new APIError("UNAUTHORIZED", {
|
|
395
|
+
message: "Bitcoin signature pubkey does not match client",
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const bodyString = new URLSearchParams(Object.entries(body).map(([k, v]) => [k, String(v)])).toString();
|
|
399
|
+
const verifyData = {
|
|
400
|
+
requestPath: "/oauth2/token",
|
|
401
|
+
timestamp: parsed.timestamp,
|
|
402
|
+
body: bodyString,
|
|
403
|
+
};
|
|
404
|
+
const isValid = verifyAuthToken(authToken, verifyData, 5);
|
|
405
|
+
if (!isValid) {
|
|
406
|
+
throw new APIError("UNAUTHORIZED", {
|
|
407
|
+
message: "Invalid Bitcoin signature",
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
debug.log(`Token refresh: client authenticated via Bitcoin signature (clientId: ${clientId})`);
|
|
411
|
+
// Inject client_id into request body for Better Auth to process
|
|
412
|
+
const modifiedBody = {
|
|
413
|
+
...ctx.body,
|
|
414
|
+
client_id: clientId,
|
|
415
|
+
};
|
|
416
|
+
return {
|
|
417
|
+
context: {
|
|
418
|
+
...ctx,
|
|
419
|
+
body: modifiedBody,
|
|
420
|
+
},
|
|
421
|
+
};
|
|
390
422
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
423
|
+
// Unknown grant type
|
|
424
|
+
throw new APIError("BAD_REQUEST", {
|
|
425
|
+
message: `Unsupported grant_type: ${grantType}`,
|
|
426
|
+
});
|
|
427
|
+
}),
|
|
428
|
+
},
|
|
429
|
+
],
|
|
430
|
+
},
|
|
431
|
+
endpoints: {
|
|
432
|
+
/**
|
|
433
|
+
* Store selected BAP ID for OAuth consent
|
|
434
|
+
*/
|
|
435
|
+
storeConsentBapId: createAuthEndpoint("/sigma/store-consent-bap-id", {
|
|
436
|
+
method: "POST",
|
|
437
|
+
body: z.object({
|
|
438
|
+
consentCode: z.string(),
|
|
439
|
+
bapId: z.string(),
|
|
408
440
|
}),
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
consentCode
|
|
420
|
-
|
|
441
|
+
use: [sessionMiddleware],
|
|
442
|
+
}, async (ctx) => {
|
|
443
|
+
// Session is guaranteed to exist due to sessionMiddleware
|
|
444
|
+
const _session = ctx.context.session;
|
|
445
|
+
// Validate options
|
|
446
|
+
if (!options?.cache) {
|
|
447
|
+
throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
448
|
+
message: "Plugin configuration error: cache not available",
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const { consentCode, bapId } = ctx.body;
|
|
452
|
+
try {
|
|
453
|
+
// Store in KV with 5 minute TTL
|
|
454
|
+
const kvKey = `consent:${consentCode}:bap_id`;
|
|
455
|
+
await options.cache.set(kvKey, bapId, { ex: 300 });
|
|
456
|
+
return ctx.json({ success: true });
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
debug.error("Error storing consent BAP ID selection:", error);
|
|
460
|
+
throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
461
|
+
message: "Failed to store identity selection",
|
|
462
|
+
});
|
|
463
|
+
}
|
|
421
464
|
}),
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const kvKey = `consent:${consentCode}:bap_id`;
|
|
436
|
-
await options.cache.set(kvKey, bapId, { ex: 300 });
|
|
437
|
-
return ctx.json({ success: true });
|
|
438
|
-
}
|
|
439
|
-
catch (error) {
|
|
440
|
-
console.error("❌ [Store Consent BAP ID] Error storing selection:", error);
|
|
441
|
-
throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
442
|
-
message: "Failed to store identity selection",
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
}),
|
|
446
|
-
signInSigma: createAuthEndpoint("/sign-in/sigma", {
|
|
447
|
-
method: "POST",
|
|
448
|
-
body: z.optional(z.object({
|
|
449
|
-
bapId: z.string().optional(),
|
|
450
|
-
})),
|
|
451
|
-
}, async (ctx) => {
|
|
452
|
-
// Get auth token from header
|
|
453
|
-
const authToken = ctx.headers?.get("x-auth-token");
|
|
454
|
-
if (!authToken) {
|
|
455
|
-
throw new APIError("UNAUTHORIZED", {
|
|
456
|
-
message: "No auth token provided",
|
|
465
|
+
signInSigma: createAuthEndpoint("/sign-in/sigma", {
|
|
466
|
+
method: "POST",
|
|
467
|
+
body: z.optional(z.object({
|
|
468
|
+
bapId: z.string().optional(),
|
|
469
|
+
})),
|
|
470
|
+
}, async (ctx) => {
|
|
471
|
+
// Debug logging for sign-in request
|
|
472
|
+
const allHeaders = {};
|
|
473
|
+
ctx.headers?.forEach((value, key) => {
|
|
474
|
+
allHeaders[key] =
|
|
475
|
+
key.toLowerCase() === "x-auth-token"
|
|
476
|
+
? `${value.substring(0, 20)}...` // Truncate sensitive token
|
|
477
|
+
: value;
|
|
457
478
|
});
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
throw new APIError("BAD_REQUEST", {
|
|
463
|
-
message: "Invalid auth token format",
|
|
479
|
+
debug.log("Sign-in request received", {
|
|
480
|
+
headers: allHeaders,
|
|
481
|
+
body: ctx.body,
|
|
482
|
+
hasAuthToken: !!ctx.headers?.get("x-auth-token"),
|
|
464
483
|
});
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
};
|
|
471
|
-
const isValid = verifyAuthToken(authToken, verifyData, 5);
|
|
472
|
-
if (!isValid) {
|
|
473
|
-
throw new APIError("UNAUTHORIZED", {
|
|
474
|
-
message: "Invalid auth token signature",
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
// Extract pubkey from the parsed token
|
|
478
|
-
const pubkey = parsed.pubkey;
|
|
479
|
-
// Try to find user by pubkey first
|
|
480
|
-
const users = await ctx.context.adapter.findMany({
|
|
481
|
-
model: "user",
|
|
482
|
-
where: [{ field: "pubkey", value: pubkey }],
|
|
483
|
-
});
|
|
484
|
-
let user = users[0];
|
|
485
|
-
// If not found by user.pubkey, check profile table for member_pubkey
|
|
486
|
-
if (!user && options?.getPool) {
|
|
487
|
-
const pool = options.getPool();
|
|
488
|
-
// Use pool.query() directly to avoid connect/release pattern
|
|
489
|
-
const profileResult = await pool.query("SELECT user_id FROM profile WHERE member_pubkey = $1 LIMIT 1", [pubkey]);
|
|
490
|
-
const profileRow = profileResult.rows[0];
|
|
491
|
-
if (profileRow) {
|
|
492
|
-
const userId = profileRow.user_id;
|
|
493
|
-
// Fetch the user record
|
|
494
|
-
const foundUsers = await ctx.context.adapter.findMany({
|
|
495
|
-
model: "user",
|
|
496
|
-
where: [{ field: "id", value: userId }],
|
|
484
|
+
// Get auth token from header
|
|
485
|
+
const authToken = ctx.headers?.get("x-auth-token");
|
|
486
|
+
if (!authToken) {
|
|
487
|
+
throw new APIError("UNAUTHORIZED", {
|
|
488
|
+
message: "No auth token provided",
|
|
497
489
|
});
|
|
498
|
-
user = foundUsers[0];
|
|
499
490
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
data: {
|
|
507
|
-
name: PublicKey.fromString(pubkey).toAddress(),
|
|
508
|
-
pubkey,
|
|
509
|
-
emailVerified: false,
|
|
510
|
-
createdAt: new Date(),
|
|
511
|
-
updatedAt: new Date(),
|
|
512
|
-
},
|
|
513
|
-
}));
|
|
491
|
+
// Parse the auth token
|
|
492
|
+
const parsed = parseAuthToken(authToken);
|
|
493
|
+
if (!parsed?.pubkey) {
|
|
494
|
+
throw new APIError("BAD_REQUEST", {
|
|
495
|
+
message: "Invalid auth token format",
|
|
496
|
+
});
|
|
514
497
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
498
|
+
// Verify the auth token
|
|
499
|
+
const verifyData = {
|
|
500
|
+
requestPath: "/api/auth/sign-in/sigma",
|
|
501
|
+
timestamp: parsed.timestamp,
|
|
502
|
+
};
|
|
503
|
+
const isValid = verifyAuthToken(authToken, verifyData, 5);
|
|
504
|
+
if (!isValid) {
|
|
505
|
+
throw new APIError("UNAUTHORIZED", {
|
|
506
|
+
message: "Invalid auth token signature",
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
// Extract pubkey from the parsed token
|
|
510
|
+
const pubkey = parsed.pubkey;
|
|
511
|
+
// Try to find user by pubkey first
|
|
512
|
+
const users = await ctx.context.adapter.findMany({
|
|
513
|
+
model: "user",
|
|
514
|
+
where: [{ field: "pubkey", value: pubkey }],
|
|
515
|
+
});
|
|
516
|
+
let user = users[0];
|
|
517
|
+
// If not found by user.pubkey, check profile table for member_pubkey
|
|
518
|
+
if (!user && options?.getPool) {
|
|
519
|
+
const pool = options.getPool();
|
|
520
|
+
// Use pool.query() directly to avoid connect/release pattern
|
|
521
|
+
const profileResult = await pool.query("SELECT user_id FROM profile WHERE member_pubkey = $1 LIMIT 1", [pubkey]);
|
|
522
|
+
const profileRow = profileResult.rows[0];
|
|
523
|
+
if (profileRow) {
|
|
524
|
+
const userId = profileRow.user_id;
|
|
525
|
+
// Fetch the user record
|
|
526
|
+
const foundUsers = await ctx.context.adapter.findMany({
|
|
522
527
|
model: "user",
|
|
523
|
-
where: [{ field: "
|
|
528
|
+
where: [{ field: "id", value: userId }],
|
|
524
529
|
});
|
|
525
|
-
user =
|
|
526
|
-
if (!user) {
|
|
527
|
-
throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
528
|
-
message: "User exists but cannot be found",
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
throw error;
|
|
530
|
+
user = foundUsers[0];
|
|
534
531
|
}
|
|
535
532
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
profileResult = await pool.query("SELECT bap_id, name, image, member_pubkey FROM profile WHERE bap_id = $1 AND user_id = $2 LIMIT 1", [selectedBapId, user.id]);
|
|
533
|
+
if (!user) {
|
|
534
|
+
// Create new user with pubkey (no email)
|
|
535
|
+
try {
|
|
536
|
+
user = (await ctx.context.adapter.create({
|
|
537
|
+
model: "user",
|
|
538
|
+
data: {
|
|
539
|
+
name: PublicKey.fromString(pubkey).toAddress(),
|
|
540
|
+
pubkey,
|
|
541
|
+
emailVerified: false,
|
|
542
|
+
createdAt: new Date(),
|
|
543
|
+
updatedAt: new Date(),
|
|
544
|
+
},
|
|
545
|
+
}));
|
|
550
546
|
}
|
|
551
|
-
|
|
552
|
-
//
|
|
553
|
-
|
|
547
|
+
catch (error) {
|
|
548
|
+
// If duplicate key error, try to find the user again by pubkey
|
|
549
|
+
if (error &&
|
|
550
|
+
typeof error === "object" &&
|
|
551
|
+
"code" in error &&
|
|
552
|
+
error.code === "23505") {
|
|
553
|
+
const existingUsers = await ctx.context.adapter.findMany({
|
|
554
|
+
model: "user",
|
|
555
|
+
where: [{ field: "pubkey", value: pubkey }],
|
|
556
|
+
});
|
|
557
|
+
user = existingUsers[0];
|
|
558
|
+
if (!user) {
|
|
559
|
+
throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
560
|
+
message: "User exists but cannot be found",
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
554
567
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
568
|
+
}
|
|
569
|
+
// Resolve BAP ID if resolver is provided
|
|
570
|
+
if (options?.resolveBAPId && options?.getPool) {
|
|
571
|
+
const pool = options.getPool();
|
|
572
|
+
const bapId = await options.resolveBAPId(pool, user.id, pubkey, true);
|
|
573
|
+
if (bapId) {
|
|
574
|
+
debug.log(`BAP ID resolved and registered: ${bapId.substring(0, 20)}...`);
|
|
575
|
+
// Update user record with profile data from profile table
|
|
576
|
+
const selectedBapId = ctx.body?.bapId;
|
|
577
|
+
let profileResult;
|
|
578
|
+
if (selectedBapId) {
|
|
579
|
+
// Query profile for selected identity
|
|
580
|
+
// Use pool.query() directly to avoid connect/release pattern
|
|
581
|
+
profileResult = await pool.query("SELECT bap_id, name, image, member_pubkey FROM profile WHERE bap_id = $1 AND user_id = $2 LIMIT 1", [selectedBapId, user.id]);
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
// Query for primary profile
|
|
585
|
+
profileResult = await pool.query("SELECT bap_id, name, image, member_pubkey FROM profile WHERE user_id = $1 AND is_primary = true LIMIT 1", [user.id]);
|
|
586
|
+
}
|
|
587
|
+
const selectedProfile = profileResult.rows[0];
|
|
588
|
+
if (selectedProfile) {
|
|
589
|
+
// Update user record with profile data
|
|
590
|
+
await ctx.context.adapter.update({
|
|
591
|
+
model: "user",
|
|
592
|
+
where: [{ field: "id", value: user.id }],
|
|
593
|
+
update: {
|
|
594
|
+
name: selectedProfile.name,
|
|
595
|
+
image: selectedProfile.image,
|
|
596
|
+
...(selectedProfile.member_pubkey && {
|
|
597
|
+
pubkey: selectedProfile.member_pubkey,
|
|
598
|
+
}),
|
|
599
|
+
updatedAt: new Date(),
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
// Re-fetch user to get updated profile data
|
|
604
|
+
const updatedUsers = await ctx.context.adapter.findMany({
|
|
559
605
|
model: "user",
|
|
560
606
|
where: [{ field: "id", value: user.id }],
|
|
561
|
-
update: {
|
|
562
|
-
name: selectedProfile.name,
|
|
563
|
-
image: selectedProfile.image,
|
|
564
|
-
...(selectedProfile.member_pubkey && {
|
|
565
|
-
pubkey: selectedProfile.member_pubkey,
|
|
566
|
-
}),
|
|
567
|
-
updatedAt: new Date(),
|
|
568
|
-
},
|
|
569
607
|
});
|
|
608
|
+
if (updatedUsers[0]) {
|
|
609
|
+
user = updatedUsers[0];
|
|
610
|
+
}
|
|
570
611
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
612
|
+
}
|
|
613
|
+
// Create session
|
|
614
|
+
const session = await ctx.context.internalAdapter.createSession(user.id);
|
|
615
|
+
if (!session) {
|
|
616
|
+
throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
617
|
+
message: "Internal Server Error",
|
|
618
|
+
status: 500,
|
|
575
619
|
});
|
|
576
|
-
if (updatedUsers[0]) {
|
|
577
|
-
user = updatedUsers[0];
|
|
578
|
-
}
|
|
579
620
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
621
|
+
await setSessionCookie(ctx, { session, user });
|
|
622
|
+
return ctx.json({
|
|
623
|
+
token: session.token,
|
|
624
|
+
user: {
|
|
625
|
+
id: user.id,
|
|
626
|
+
pubkey: user.pubkey,
|
|
627
|
+
name: user.name,
|
|
628
|
+
},
|
|
587
629
|
});
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
user: {
|
|
593
|
-
id: user.id,
|
|
594
|
-
pubkey: user.pubkey,
|
|
595
|
-
name: user.name,
|
|
596
|
-
},
|
|
597
|
-
});
|
|
598
|
-
}),
|
|
599
|
-
},
|
|
600
|
-
});
|
|
630
|
+
}),
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
};
|
|
601
634
|
//# sourceMappingURL=index.js.map
|