@sigma-auth/better-auth-plugin 0.0.1

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.
@@ -0,0 +1,717 @@
1
+ import { PublicKey } from "@bsv/sdk";
2
+ import { APIError, createAuthEndpoint, sessionMiddleware, } from "better-auth/api";
3
+ import { setSessionCookie } from "better-auth/cookies";
4
+ import { createAuthMiddleware } from "better-auth/plugins";
5
+ import { parseAuthToken, verifyAuthToken } from "bitcoin-auth";
6
+ import { z } from "zod";
7
+ /**
8
+ * Sigma Auth provider plugin for Better Auth
9
+ * This is the OAuth provider that runs on auth.sigmaidentity.com
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { betterAuth } from "better-auth";
14
+ * import { sigmaProvider } from "@sigma-auth/better-auth-plugin/provider";
15
+ *
16
+ * export const auth = betterAuth({
17
+ * plugins: [
18
+ * sigmaProvider({
19
+ * getPool: () => dbPool,
20
+ * cache: redisCache,
21
+ * resolveBAPId: async (pool, userId, pubkey, register) => {
22
+ * // Custom BAP ID resolution logic
23
+ * },
24
+ * }),
25
+ * ],
26
+ * });
27
+ * ```
28
+ */
29
+ export const sigmaProvider = (options) => ({
30
+ id: "sigma",
31
+ schema: {
32
+ user: {
33
+ fields: {
34
+ pubkey: {
35
+ type: "string",
36
+ required: true,
37
+ unique: true,
38
+ },
39
+ ...(options?.enableSubscription
40
+ ? {
41
+ subscriptionTier: {
42
+ type: "string",
43
+ required: false,
44
+ defaultValue: "free",
45
+ },
46
+ }
47
+ : {}),
48
+ },
49
+ },
50
+ session: {
51
+ fields: {
52
+ ...(options?.enableSubscription
53
+ ? {
54
+ subscriptionTier: {
55
+ type: "string",
56
+ required: false,
57
+ },
58
+ }
59
+ : {}),
60
+ },
61
+ },
62
+ oauthAccessToken: {
63
+ fields: {
64
+ selectedBapId: {
65
+ type: "string",
66
+ required: false,
67
+ },
68
+ },
69
+ },
70
+ oauthApplication: {
71
+ fields: {
72
+ owner_bap_id: {
73
+ type: "string",
74
+ required: true,
75
+ },
76
+ },
77
+ },
78
+ oauthConsent: {
79
+ fields: {
80
+ selectedBapId: {
81
+ type: "string",
82
+ required: false,
83
+ },
84
+ },
85
+ },
86
+ },
87
+ hooks: {
88
+ after: [
89
+ {
90
+ matcher: (ctx) => ctx.path === "/oauth2/token",
91
+ handler: createAuthMiddleware(async (ctx) => {
92
+ console.log("🔵 [Sigma Plugin] AFTER hook triggered for /oauth2/token");
93
+ const body = ctx.body;
94
+ const grantType = body.grant_type;
95
+ // Only handle authorization_code grant (not refresh_token)
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) {
137
+ return;
138
+ }
139
+ const selectedBapId = consentRecord.selectedBapId;
140
+ // Update the oauthAccessToken record with the selected BAP ID
141
+ await ctx.context.adapter.update({
142
+ model: "oauthAccessToken",
143
+ where: [{ field: "accessToken", value: accessToken }],
144
+ update: {
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
+ const client = await pool.connect();
153
+ try {
154
+ // Query profile table for selected identity
155
+ const profileResult = await client.query("SELECT bap_id, name, image, member_pubkey FROM profile WHERE bap_id = $1 AND user_id = $2 LIMIT 1", [selectedBapId, userId]);
156
+ const profile = profileResult.rows[0];
157
+ if (profile) {
158
+ // Update user record with profile data
159
+ await ctx.context.adapter.update({
160
+ model: "user",
161
+ where: [{ field: "id", value: userId }],
162
+ update: {
163
+ name: profile.name,
164
+ image: profile.image,
165
+ ...(profile.member_pubkey && {
166
+ pubkey: profile.member_pubkey,
167
+ }),
168
+ updatedAt: new Date(),
169
+ },
170
+ });
171
+ }
172
+ }
173
+ finally {
174
+ client.release();
175
+ }
176
+ }
177
+ }
178
+ catch (error) {
179
+ console.error("❌ [OAuth Token] Error storing identity selection:", error);
180
+ }
181
+ }),
182
+ },
183
+ {
184
+ matcher: (ctx) => ctx.path === "/oauth2/userinfo",
185
+ handler: createAuthMiddleware(async (ctx) => {
186
+ // Get the access token from Authorization header
187
+ const authHeader = ctx.headers?.get?.("authorization");
188
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
189
+ return;
190
+ }
191
+ const accessToken = authHeader.substring(7);
192
+ try {
193
+ // Look up the access token record to get selectedBapId using adapter
194
+ const tokenRecords = await ctx.context.adapter.findMany({
195
+ model: "oauthAccessToken",
196
+ where: [{ field: "accessToken", value: accessToken }],
197
+ limit: 1,
198
+ });
199
+ const tokenRecord = tokenRecords[0];
200
+ if (!tokenRecord || !tokenRecord.selectedBapId) {
201
+ return; // No selected BAP ID, use primary (default behavior)
202
+ }
203
+ const selectedBapId = tokenRecord.selectedBapId;
204
+ // Get BAP ID details from profile table using getPool if available
205
+ if (!options?.getPool) {
206
+ return;
207
+ }
208
+ const pool = options.getPool();
209
+ const client = await pool.connect();
210
+ try {
211
+ const bapResult = await client.query("SELECT bap_id, name, image, profile FROM profile WHERE bap_id = $1 LIMIT 1", [selectedBapId]);
212
+ if (bapResult.rows.length === 0) {
213
+ throw new Error(`Selected identity not found: ${selectedBapId}`);
214
+ }
215
+ const selectedName = bapResult.rows[0].name;
216
+ let selectedImage = bapResult.rows[0].image;
217
+ let profileData = bapResult.rows[0].profile;
218
+ // If profile JSONB is NULL, fetch from blockchain and populate it
219
+ if (!profileData) {
220
+ try {
221
+ const profileResponse = await fetch("https://api.sigmaidentity.com/api/v1/identity/get", {
222
+ method: "POST",
223
+ headers: { "Content-Type": "application/json" },
224
+ body: JSON.stringify({ idKey: selectedBapId }),
225
+ });
226
+ if (profileResponse.ok) {
227
+ const apiData = (await profileResponse.json());
228
+ if (apiData.result) {
229
+ profileData = apiData.result;
230
+ // Update database with complete profile JSONB
231
+ await client.query(`UPDATE profile SET
232
+ profile = $1,
233
+ image = COALESCE($2, image),
234
+ updated_at = NOW()
235
+ WHERE bap_id = $3`, [
236
+ JSON.stringify(profileData),
237
+ profileData.identity?.image || null,
238
+ selectedBapId,
239
+ ]);
240
+ if (profileData.identity?.image) {
241
+ selectedImage = profileData.identity.image;
242
+ }
243
+ }
244
+ }
245
+ }
246
+ catch (fetchError) {
247
+ console.error(`❌ [OAuth Userinfo] Failed to fetch profile:`, fetchError);
248
+ }
249
+ }
250
+ // Modify the response to use selected BAP ID instead of primary
251
+ const responseBody = ctx.context.returned;
252
+ if (responseBody && typeof responseBody === "object") {
253
+ // Fetch the member pubkey for this BAP ID from KV reverse index
254
+ let memberPubkey = null;
255
+ if (options.cache) {
256
+ try {
257
+ const reverseKey = `bap:member_pubkey:${selectedBapId}`;
258
+ memberPubkey = await options.cache.get(reverseKey);
259
+ }
260
+ catch (error) {
261
+ console.error(`❌ [OAuth Userinfo] Error fetching member pubkey:`, error);
262
+ }
263
+ }
264
+ // Return the modified userinfo response
265
+ return {
266
+ ...responseBody,
267
+ // Standard OIDC claims
268
+ name: selectedName,
269
+ given_name: profileData?.identity?.givenName || selectedName,
270
+ family_name: profileData?.identity?.familyName || null,
271
+ picture: selectedImage || responseBody.picture || null,
272
+ // Custom claims
273
+ pubkey: memberPubkey || responseBody.pubkey,
274
+ bap: profileData || null,
275
+ };
276
+ }
277
+ }
278
+ finally {
279
+ client.release();
280
+ }
281
+ }
282
+ catch (error) {
283
+ console.error("❌ [OAuth Userinfo] Error retrieving selected BAP ID:", error);
284
+ }
285
+ }),
286
+ },
287
+ {
288
+ matcher: (ctx) => ctx.path === "/oauth2/consent",
289
+ handler: createAuthMiddleware(async (ctx) => {
290
+ // Only proceed if we have cache option
291
+ if (!options?.cache) {
292
+ return;
293
+ }
294
+ const body = ctx.body;
295
+ const consentCode = body.consent_code;
296
+ const accept = body.accept;
297
+ // Only store selectedBapId if consent was accepted
298
+ if (!accept || !consentCode) {
299
+ return;
300
+ }
301
+ try {
302
+ // Get session for userId
303
+ const session = ctx.context.session;
304
+ if (!session?.user?.id) {
305
+ return;
306
+ }
307
+ // Wait a bit for Better Auth to create the consent record
308
+ await new Promise((resolve) => setTimeout(resolve, 100));
309
+ // Query the database to get the clientId from the consent record
310
+ const consentRecords = await ctx.context.adapter.findMany({
311
+ model: "oauthConsent",
312
+ where: [{ field: "userId", value: session.user.id }],
313
+ limit: 1,
314
+ sortBy: { field: "createdAt", direction: "desc" },
315
+ });
316
+ const consentRecord = consentRecords[0];
317
+ if (!consentRecord) {
318
+ return;
319
+ }
320
+ const { id: consentId } = consentRecord;
321
+ // Retrieve selected BAP ID from cache/KV
322
+ const selectedBapId = await options.cache.get(`consent:${consentCode}:bap_id`);
323
+ if (!selectedBapId) {
324
+ return;
325
+ }
326
+ // Update the consent record with selectedBapId using adapter
327
+ await ctx.context.adapter.update({
328
+ model: "oauthConsent",
329
+ where: [{ field: "id", value: consentId }],
330
+ update: {
331
+ selectedBapId,
332
+ },
333
+ });
334
+ console.log(`✅ [OAuth Consent Hook] Stored BAP ID in consent: user=${session.user.id.substring(0, 15)}... bap=${selectedBapId.substring(0, 15)}...`);
335
+ }
336
+ catch (error) {
337
+ console.error("❌ [OAuth Consent Hook] Error storing identity selection:", error);
338
+ }
339
+ }),
340
+ },
341
+ ],
342
+ before: [
343
+ {
344
+ matcher: (ctx) => ctx.path === "/oauth2/token",
345
+ handler: createAuthMiddleware(async (ctx) => {
346
+ const body = ctx.body;
347
+ const grantType = body.grant_type;
348
+ // Handle authorization_code grant type (exchange code for token)
349
+ if (grantType === "authorization_code") {
350
+ // Get client_id from request body
351
+ const clientId = body.client_id;
352
+ if (!clientId) {
353
+ throw new APIError("BAD_REQUEST", {
354
+ message: "Missing client_id in request body",
355
+ });
356
+ }
357
+ // Lookup OAuth client by client_id
358
+ const clients = await ctx.context.adapter.findMany({
359
+ model: "oauthApplication",
360
+ where: [{ field: "clientId", value: clientId }],
361
+ });
362
+ if (clients.length === 0) {
363
+ throw new APIError("UNAUTHORIZED", {
364
+ message: `OAuth client not registered: ${clientId}`,
365
+ });
366
+ }
367
+ const client = clients[0];
368
+ // Validate client authentication via Bitcoin signature
369
+ const headers = new Headers(ctx.headers || {});
370
+ const authToken = headers.get("x-auth-token");
371
+ if (!authToken) {
372
+ throw new APIError("UNAUTHORIZED", {
373
+ message: "Missing X-Auth-Token header for client authentication",
374
+ });
375
+ }
376
+ // Parse the auth token to extract pubkey
377
+ const parsed = parseAuthToken(authToken);
378
+ if (!parsed?.pubkey) {
379
+ throw new APIError("UNAUTHORIZED", {
380
+ message: "Invalid Bitcoin auth token format",
381
+ });
382
+ }
383
+ // Verify the pubkey from signature matches the client's memberPubkey
384
+ if (!client.metadata) {
385
+ throw new APIError("UNAUTHORIZED", {
386
+ message: `Client ${clientId} has no metadata`,
387
+ });
388
+ }
389
+ const metadata = JSON.parse(client.metadata);
390
+ const expectedPubkey = metadata.memberPubkey;
391
+ if (!expectedPubkey) {
392
+ throw new APIError("UNAUTHORIZED", {
393
+ message: `Client ${clientId} has no memberPubkey in metadata`,
394
+ });
395
+ }
396
+ if (parsed.pubkey !== expectedPubkey) {
397
+ throw new APIError("UNAUTHORIZED", {
398
+ message: "Bitcoin signature pubkey does not match client",
399
+ });
400
+ }
401
+ // Get request body for signature verification
402
+ const bodyString = new URLSearchParams(Object.entries(body).map(([k, v]) => [k, String(v)])).toString();
403
+ // Verify Bitcoin signature with body
404
+ const verifyData = {
405
+ requestPath: "/oauth2/token",
406
+ timestamp: parsed.timestamp,
407
+ body: bodyString,
408
+ };
409
+ const isValid = verifyAuthToken(authToken, verifyData, 5);
410
+ if (!isValid) {
411
+ throw new APIError("UNAUTHORIZED", {
412
+ message: "Invalid Bitcoin signature",
413
+ });
414
+ }
415
+ console.log(`✅ [OAuth Token] Client authenticated via Bitcoin signature (clientId: ${clientId})`);
416
+ // Inject client_id into request body for Better Auth to process
417
+ const modifiedBody = {
418
+ ...ctx.body,
419
+ client_id: clientId,
420
+ };
421
+ return {
422
+ context: {
423
+ ...ctx,
424
+ body: modifiedBody,
425
+ },
426
+ };
427
+ }
428
+ // Handle refresh_token grant type
429
+ if (grantType === "refresh_token") {
430
+ const refreshToken = body.refresh_token;
431
+ if (!refreshToken) {
432
+ throw new APIError("BAD_REQUEST", {
433
+ message: "Missing refresh_token",
434
+ });
435
+ }
436
+ // Get client_id from request body
437
+ const clientId = body.client_id;
438
+ if (!clientId) {
439
+ throw new APIError("BAD_REQUEST", {
440
+ message: "Missing client_id in request body",
441
+ });
442
+ }
443
+ // Lookup OAuth client by client_id
444
+ const clients = await ctx.context.adapter.findMany({
445
+ model: "oauthApplication",
446
+ where: [{ field: "clientId", value: clientId }],
447
+ });
448
+ if (clients.length === 0) {
449
+ throw new APIError("UNAUTHORIZED", {
450
+ message: `OAuth client not registered: ${clientId}`,
451
+ });
452
+ }
453
+ const client = clients[0];
454
+ // Validate client signature first
455
+ const headers = new Headers(ctx.headers || {});
456
+ const authToken = headers.get("x-auth-token");
457
+ if (!authToken) {
458
+ throw new APIError("UNAUTHORIZED", {
459
+ message: "Missing X-Auth-Token header for client authentication",
460
+ });
461
+ }
462
+ const parsed = parseAuthToken(authToken);
463
+ if (!parsed?.pubkey) {
464
+ throw new APIError("UNAUTHORIZED", {
465
+ message: "Invalid Bitcoin auth token format",
466
+ });
467
+ }
468
+ if (!client.metadata) {
469
+ throw new APIError("UNAUTHORIZED", {
470
+ message: `Client ${clientId} has no metadata`,
471
+ });
472
+ }
473
+ const metadata = JSON.parse(client.metadata);
474
+ const expectedPubkey = metadata.memberPubkey;
475
+ if (!expectedPubkey) {
476
+ throw new APIError("UNAUTHORIZED", {
477
+ message: `Client ${clientId} has no memberPubkey in metadata`,
478
+ });
479
+ }
480
+ if (parsed.pubkey !== expectedPubkey) {
481
+ throw new APIError("UNAUTHORIZED", {
482
+ message: "Bitcoin signature pubkey does not match client",
483
+ });
484
+ }
485
+ const bodyString = new URLSearchParams(Object.entries(body).map(([k, v]) => [k, String(v)])).toString();
486
+ const verifyData = {
487
+ requestPath: "/oauth2/token",
488
+ timestamp: parsed.timestamp,
489
+ body: bodyString,
490
+ };
491
+ const isValid = verifyAuthToken(authToken, verifyData, 5);
492
+ if (!isValid) {
493
+ throw new APIError("UNAUTHORIZED", {
494
+ message: "Invalid Bitcoin signature",
495
+ });
496
+ }
497
+ console.log(`✅ [OAuth Token Refresh] Client authenticated via Bitcoin signature (clientId: ${clientId})`);
498
+ // Inject client_id into request body for Better Auth to process
499
+ const modifiedBody = {
500
+ ...ctx.body,
501
+ client_id: clientId,
502
+ };
503
+ return {
504
+ context: {
505
+ ...ctx,
506
+ body: modifiedBody,
507
+ },
508
+ };
509
+ }
510
+ // Unknown grant type
511
+ throw new APIError("BAD_REQUEST", {
512
+ message: `Unsupported grant_type: ${grantType}`,
513
+ });
514
+ }),
515
+ },
516
+ ],
517
+ },
518
+ endpoints: {
519
+ /**
520
+ * Store selected BAP ID for OAuth consent
521
+ */
522
+ storeConsentBapId: createAuthEndpoint("/sigma/store-consent-bap-id", {
523
+ method: "POST",
524
+ body: z.object({
525
+ consentCode: z.string(),
526
+ bapId: z.string(),
527
+ }),
528
+ use: [sessionMiddleware],
529
+ }, async (ctx) => {
530
+ // Session is guaranteed to exist due to sessionMiddleware
531
+ const _session = ctx.context.session;
532
+ // Validate options
533
+ if (!options?.cache) {
534
+ throw new APIError("INTERNAL_SERVER_ERROR", {
535
+ message: "Plugin configuration error: cache not available",
536
+ });
537
+ }
538
+ const { consentCode, bapId } = ctx.body;
539
+ try {
540
+ // Store in KV with 5 minute TTL
541
+ const kvKey = `consent:${consentCode}:bap_id`;
542
+ await options.cache.set(kvKey, bapId, { ex: 300 });
543
+ return ctx.json({ success: true });
544
+ }
545
+ catch (error) {
546
+ console.error("❌ [Store Consent BAP ID] Error storing selection:", error);
547
+ throw new APIError("INTERNAL_SERVER_ERROR", {
548
+ message: "Failed to store identity selection",
549
+ });
550
+ }
551
+ }),
552
+ signInSigma: createAuthEndpoint("/sign-in/sigma", {
553
+ method: "POST",
554
+ body: z.optional(z.object({
555
+ bapId: z.string().optional(),
556
+ })),
557
+ }, async (ctx) => {
558
+ // Get auth token from header
559
+ const authToken = ctx.headers?.get("x-auth-token");
560
+ if (!authToken) {
561
+ throw new APIError("UNAUTHORIZED", {
562
+ message: "No auth token provided",
563
+ });
564
+ }
565
+ // Parse the auth token
566
+ const parsed = parseAuthToken(authToken);
567
+ if (!parsed?.pubkey) {
568
+ throw new APIError("BAD_REQUEST", {
569
+ message: "Invalid auth token format",
570
+ });
571
+ }
572
+ // Verify the auth token
573
+ const verifyData = {
574
+ requestPath: "/api/auth/sign-in/sigma",
575
+ timestamp: parsed.timestamp,
576
+ };
577
+ const isValid = verifyAuthToken(authToken, verifyData, 5);
578
+ if (!isValid) {
579
+ throw new APIError("UNAUTHORIZED", {
580
+ message: "Invalid auth token signature",
581
+ });
582
+ }
583
+ // Extract pubkey from the parsed token
584
+ const pubkey = parsed.pubkey;
585
+ // Try to find user by pubkey first
586
+ const users = await ctx.context.adapter.findMany({
587
+ model: "user",
588
+ where: [{ field: "pubkey", value: pubkey }],
589
+ });
590
+ let user = users[0];
591
+ // If not found by user.pubkey, check profile table for member_pubkey
592
+ if (!user && options?.getPool) {
593
+ const pool = options.getPool();
594
+ const client = await pool.connect();
595
+ try {
596
+ const profileResult = await client.query("SELECT user_id FROM profile WHERE member_pubkey = $1 LIMIT 1", [pubkey]);
597
+ const profileRow = profileResult.rows[0];
598
+ if (profileRow) {
599
+ const userId = profileRow.user_id;
600
+ // Fetch the user record
601
+ const foundUsers = await ctx.context.adapter.findMany({
602
+ model: "user",
603
+ where: [{ field: "id", value: userId }],
604
+ });
605
+ user = foundUsers[0];
606
+ }
607
+ }
608
+ finally {
609
+ client.release();
610
+ }
611
+ }
612
+ if (!user) {
613
+ // Create new user with pubkey (no email)
614
+ try {
615
+ user = (await ctx.context.adapter.create({
616
+ model: "user",
617
+ data: {
618
+ name: PublicKey.fromString(pubkey).toAddress(),
619
+ pubkey,
620
+ emailVerified: false,
621
+ createdAt: new Date(),
622
+ updatedAt: new Date(),
623
+ },
624
+ }));
625
+ }
626
+ catch (error) {
627
+ // If duplicate key error, try to find the user again by pubkey
628
+ if (error &&
629
+ typeof error === "object" &&
630
+ "code" in error &&
631
+ error.code === "23505") {
632
+ const existingUsers = await ctx.context.adapter.findMany({
633
+ model: "user",
634
+ where: [{ field: "pubkey", value: pubkey }],
635
+ });
636
+ user = existingUsers[0];
637
+ if (!user) {
638
+ throw new APIError("INTERNAL_SERVER_ERROR", {
639
+ message: "User exists but cannot be found",
640
+ });
641
+ }
642
+ }
643
+ else {
644
+ throw error;
645
+ }
646
+ }
647
+ }
648
+ // Resolve BAP ID if resolver is provided
649
+ if (options?.resolveBAPId && options?.getPool) {
650
+ const pool = options.getPool();
651
+ const bapId = await options.resolveBAPId(pool, user.id, pubkey, true);
652
+ if (bapId) {
653
+ console.log(`✅ BAP ID resolved and registered: ${bapId.substring(0, 20)}...`);
654
+ // Update user record with profile data from profile table
655
+ const selectedBapId = ctx.body?.bapId;
656
+ const client = await pool.connect();
657
+ try {
658
+ let profileResult;
659
+ if (selectedBapId) {
660
+ // Query profile for selected identity
661
+ profileResult = await client.query("SELECT bap_id, name, image, member_pubkey FROM profile WHERE bap_id = $1 AND user_id = $2 LIMIT 1", [selectedBapId, user.id]);
662
+ }
663
+ else {
664
+ // Query for primary profile
665
+ profileResult = await client.query("SELECT bap_id, name, image, member_pubkey FROM profile WHERE user_id = $1 AND is_primary = true LIMIT 1", [user.id]);
666
+ }
667
+ const selectedProfile = profileResult.rows[0];
668
+ if (selectedProfile) {
669
+ // Update user record with profile data
670
+ await ctx.context.adapter.update({
671
+ model: "user",
672
+ where: [{ field: "id", value: user.id }],
673
+ update: {
674
+ name: selectedProfile.name,
675
+ image: selectedProfile.image,
676
+ ...(selectedProfile.member_pubkey && {
677
+ pubkey: selectedProfile.member_pubkey,
678
+ }),
679
+ updatedAt: new Date(),
680
+ },
681
+ });
682
+ }
683
+ }
684
+ finally {
685
+ client.release();
686
+ }
687
+ // Re-fetch user to get updated profile data
688
+ const updatedUsers = await ctx.context.adapter.findMany({
689
+ model: "user",
690
+ where: [{ field: "id", value: user.id }],
691
+ });
692
+ if (updatedUsers[0]) {
693
+ user = updatedUsers[0];
694
+ }
695
+ }
696
+ }
697
+ // Create session
698
+ const session = await ctx.context.internalAdapter.createSession(user.id, ctx);
699
+ if (!session) {
700
+ throw new APIError("INTERNAL_SERVER_ERROR", {
701
+ message: "Internal Server Error",
702
+ status: 500,
703
+ });
704
+ }
705
+ await setSessionCookie(ctx, { session, user });
706
+ return ctx.json({
707
+ token: session.token,
708
+ user: {
709
+ id: user.id,
710
+ pubkey: user.pubkey,
711
+ name: user.name,
712
+ },
713
+ });
714
+ }),
715
+ },
716
+ });
717
+ //# sourceMappingURL=index.js.map