@payez/next-mvp 4.1.14 → 4.1.21

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.
Files changed (116) hide show
  1. package/dist/api-handlers/account/profile.d.ts +38 -0
  2. package/dist/api-handlers/account/profile.js +73 -45
  3. package/dist/api-handlers/account/recovery/send-code.d.ts +33 -0
  4. package/dist/api-handlers/account/recovery/send-code.js +52 -18
  5. package/dist/api-handlers/account/recovery/verify-code.d.ts +33 -0
  6. package/dist/api-handlers/account/recovery/verify-code.js +52 -18
  7. package/dist/api-handlers/account/verify-email.js +15 -1
  8. package/dist/api-handlers/account/verify-sms.js +15 -1
  9. package/dist/api-handlers/auth/passkey-signin.d.ts +29 -0
  10. package/dist/api-handlers/auth/passkey-signin.js +97 -0
  11. package/dist/api-handlers/auth/refresh.js +38 -15
  12. package/dist/api-handlers/mfa/backup-code-verify.d.ts +73 -0
  13. package/dist/api-handlers/mfa/backup-code-verify.js +153 -0
  14. package/dist/api-handlers/mfa/backup-codes.d.ts +7 -0
  15. package/dist/api-handlers/mfa/backup-codes.js +40 -0
  16. package/dist/api-handlers/mfa/methods.d.ts +3 -0
  17. package/dist/api-handlers/mfa/methods.js +41 -0
  18. package/dist/api-handlers/mfa/totp-confirm.d.ts +8 -0
  19. package/dist/api-handlers/mfa/totp-confirm.js +41 -0
  20. package/dist/api-handlers/mfa/totp-enroll.d.ts +8 -0
  21. package/dist/api-handlers/mfa/totp-enroll.js +41 -0
  22. package/dist/api-handlers/mfa/totp-verify.d.ts +47 -0
  23. package/dist/api-handlers/mfa/totp-verify.js +134 -0
  24. package/dist/api-handlers/passkeys/list.d.ts +7 -0
  25. package/dist/api-handlers/passkeys/list.js +29 -0
  26. package/dist/api-handlers/passkeys/manage.d.ts +3 -0
  27. package/dist/api-handlers/passkeys/manage.js +54 -0
  28. package/dist/api-handlers/passkeys/register-options.d.ts +8 -0
  29. package/dist/api-handlers/passkeys/register-options.js +41 -0
  30. package/dist/api-handlers/passkeys/register-verify.d.ts +7 -0
  31. package/dist/api-handlers/passkeys/register-verify.js +40 -0
  32. package/dist/api-handlers/session/viability.js +19 -0
  33. package/dist/auth/better-auth.d.ts +68 -2
  34. package/dist/auth/better-auth.js +137 -18
  35. package/dist/auth/utils/idp-client.d.ts +13 -22
  36. package/dist/auth/utils/idp-client.js +12 -82
  37. package/dist/client/better-auth-client.d.ts +62 -62
  38. package/dist/lib/ensure-fresh-access-token.js +49 -8
  39. package/dist/lib/session-store.d.ts +1 -0
  40. package/dist/lib/session-store.js +19 -8
  41. package/dist/lib/token-lifecycle.d.ts +2 -0
  42. package/dist/lib/token-lifecycle.js +61 -7
  43. package/dist/models/SessionModel.d.ts +17 -0
  44. package/dist/pages/login/page.d.ts +10 -1
  45. package/dist/pages/login/page.js +69 -80
  46. package/dist/pages/verify-code/page.d.ts +10 -1
  47. package/dist/pages/verify-code/page.js +72 -4
  48. package/dist/routes/auth/passkey-signin.d.ts +13 -0
  49. package/dist/routes/auth/passkey-signin.js +18 -0
  50. package/dist/routes/auth/viability.d.ts +2 -0
  51. package/dist/routes/auth/viability.js +88 -0
  52. package/dist/routes/mfa/backup-code-verify.d.ts +1 -0
  53. package/dist/routes/mfa/backup-code-verify.js +5 -0
  54. package/dist/routes/mfa/backup-codes.d.ts +10 -0
  55. package/dist/routes/mfa/backup-codes.js +14 -0
  56. package/dist/routes/mfa/methods.d.ts +21 -0
  57. package/dist/routes/mfa/methods.js +10 -0
  58. package/dist/routes/mfa/totp-confirm.d.ts +10 -0
  59. package/dist/routes/mfa/totp-confirm.js +14 -0
  60. package/dist/routes/mfa/totp-enroll.d.ts +10 -0
  61. package/dist/routes/mfa/totp-enroll.js +14 -0
  62. package/dist/routes/mfa/totp-verify.d.ts +14 -0
  63. package/dist/routes/mfa/totp-verify.js +18 -0
  64. package/dist/routes/passkeys/list.d.ts +10 -0
  65. package/dist/routes/passkeys/list.js +14 -0
  66. package/dist/routes/passkeys/manage.d.ts +18 -0
  67. package/dist/routes/passkeys/manage.js +13 -0
  68. package/dist/routes/passkeys/register-options.d.ts +10 -0
  69. package/dist/routes/passkeys/register-options.js +14 -0
  70. package/dist/routes/passkeys/register-verify.d.ts +10 -0
  71. package/dist/routes/passkeys/register-verify.js +14 -0
  72. package/dist/server/auth.d.ts +29 -20
  73. package/dist/server/auth.js +8 -0
  74. package/dist/server/decode-session.js +37 -3
  75. package/package.json +958 -894
  76. package/scripts/check-internal-url-usage.sh +73 -73
  77. package/src/api-handlers/account/profile.ts +76 -57
  78. package/src/api-handlers/account/recovery/send-code.ts +65 -17
  79. package/src/api-handlers/account/recovery/verify-code.ts +65 -17
  80. package/src/api-handlers/account/verify-email.ts +15 -1
  81. package/src/api-handlers/account/verify-sms.ts +15 -1
  82. package/src/api-handlers/auth/passkey-signin.ts +98 -0
  83. package/src/api-handlers/auth/refresh.ts +38 -15
  84. package/src/api-handlers/mfa/backup-code-verify.ts +175 -0
  85. package/src/api-handlers/mfa/backup-codes.ts +41 -0
  86. package/src/api-handlers/mfa/methods.ts +46 -0
  87. package/src/api-handlers/mfa/totp-confirm.ts +42 -0
  88. package/src/api-handlers/mfa/totp-enroll.ts +42 -0
  89. package/src/api-handlers/mfa/totp-verify.ts +156 -0
  90. package/src/api-handlers/passkeys/list.ts +31 -0
  91. package/src/api-handlers/passkeys/manage.ts +54 -0
  92. package/src/api-handlers/passkeys/register-options.ts +42 -0
  93. package/src/api-handlers/passkeys/register-verify.ts +41 -0
  94. package/src/api-handlers/session/viability.ts +19 -0
  95. package/src/auth/better-auth.ts +158 -18
  96. package/src/auth/utils/idp-client.ts +13 -95
  97. package/src/lib/ensure-fresh-access-token.ts +49 -8
  98. package/src/lib/session-store.ts +11 -1
  99. package/src/lib/token-lifecycle.ts +70 -7
  100. package/src/models/SessionModel.ts +18 -0
  101. package/src/pages/login/page.tsx +123 -228
  102. package/src/pages/verify-code/page.tsx +71 -5
  103. package/src/routes/auth/passkey-signin.ts +13 -0
  104. package/src/routes/auth/viability.ts +89 -0
  105. package/src/routes/mfa/backup-code-verify.ts +1 -0
  106. package/src/routes/mfa/backup-codes.ts +10 -0
  107. package/src/routes/mfa/methods.ts +22 -0
  108. package/src/routes/mfa/totp-confirm.ts +10 -0
  109. package/src/routes/mfa/totp-enroll.ts +10 -0
  110. package/src/routes/mfa/totp-verify.ts +14 -0
  111. package/src/routes/passkeys/list.ts +10 -0
  112. package/src/routes/passkeys/manage.ts +23 -0
  113. package/src/routes/passkeys/register-options.ts +10 -0
  114. package/src/routes/passkeys/register-verify.ts +10 -0
  115. package/src/server/auth.ts +17 -1
  116. package/src/server/decode-session.ts +36 -3
@@ -51,6 +51,7 @@ exports.configureBetterAuth = configureBetterAuth;
51
51
  exports.getBetterAuthInstance = getBetterAuthInstance;
52
52
  exports.getBetterAuthHandler = getBetterAuthHandler;
53
53
  exports.exchangeOAuthForIdpTokens = exchangeOAuthForIdpTokens;
54
+ exports.storeIdpTokensInSession = storeIdpTokensInSession;
54
55
  exports.createAuthGetHandler = createAuthGetHandler;
55
56
  require("server-only");
56
57
  const node_async_hooks_1 = require("node:async_hooks");
@@ -62,6 +63,7 @@ const magic_link_1 = require("better-auth/plugins/magic-link");
62
63
  const idp_client_config_1 = require("../lib/idp-client-config");
63
64
  const app_slug_1 = require("../lib/app-slug");
64
65
  const redis_1 = require("../lib/redis");
66
+ const session_store_1 = require("../lib/session-store");
65
67
  /**
66
68
  * Build Better Auth social providers from IDP config.
67
69
  */
@@ -210,16 +212,24 @@ function createBetterAuthInstance(idpConfig, opts = {}) {
210
212
  // Redis-backed session storage via secondaryStorage
211
213
  secondaryStorage: {
212
214
  get: async (key) => {
213
- return await (0, redis_1.getRedis)().get(`ba:${appSlug}:${key}`);
215
+ try {
216
+ return await (0, redis_1.getRedis)().get(`ba:${appSlug}:${key}`);
217
+ }
218
+ catch {
219
+ return null;
220
+ }
214
221
  },
215
222
  set: async (key, value, ttl) => {
216
- const redis = (0, redis_1.getRedis)();
217
- if (ttl) {
218
- await redis.setex(`ba:${appSlug}:${key}`, ttl, value);
219
- }
220
- else {
221
- await redis.setex(`ba:${appSlug}:${key}`, 7 * 24 * 60 * 60, value);
223
+ try {
224
+ const redis = (0, redis_1.getRedis)();
225
+ if (ttl) {
226
+ await redis.setex(`ba:${appSlug}:${key}`, ttl, value);
227
+ }
228
+ else {
229
+ await redis.setex(`ba:${appSlug}:${key}`, session_store_1.SESSION_TTL, value);
230
+ }
222
231
  }
232
+ catch { /* Redis unavailable — cookie cache still works */ }
223
233
  },
224
234
  delete: async (key) => {
225
235
  try {
@@ -264,6 +274,9 @@ function createBetterAuthInstance(idpConfig, opts = {}) {
264
274
  plugins: [
265
275
  (0, next_js_1.nextCookies)(),
266
276
  ...(opts.magicLink ? [(0, magic_link_1.magicLink)(opts.magicLink)] : []),
277
+ // App-supplied plugins last: they may depend on the built-ins above, and
278
+ // ordering is BA's own concern once they are in the array.
279
+ ...(opts.plugins ?? []),
267
280
  ],
268
281
  });
269
282
  }
@@ -427,8 +440,20 @@ async function exchangeOAuthForIdpTokens(sessionToken, provider = 'google') {
427
440
  return false;
428
441
  }
429
442
  // Build IDP token data
430
- // CRITICAL: If the IDP withholds refresh_token without setting requiresTwoFactor,
431
- // treat missing refresh token as "2FA required" so middleware redirects to verify-code.
443
+ // ORIGINAL INTENT: IDP withholds refresh_token until 2FA is complete, so
444
+ // !hasRefreshToken was a reliable "2FA required" signal.
445
+ //
446
+ // BUG [IDP-2FA-BUG]: IDP is currently returning refresh_token on the initial
447
+ // oauth-callback even for require_2fa tenants that haven't done 2FA yet.
448
+ // This made !hasRefreshToken=false → requiresTwoFactor=false → mfaVerified=true,
449
+ // bypassing the 2FA gate entirely.
450
+ //
451
+ // FIX: Always set mfaVerified=false on the initial OAuth exchange.
452
+ // The tenant's require_2fa setting is enforced by the viability endpoint
453
+ // (tenantRequiresTwoFactor=true), which redirects to verify-code whenever
454
+ // mfaVerified=false. After real 2FA, transitionTo2FASession() sets it true.
455
+ // Revert this comment + restore `mfaVerified: !requiresTwoFactor` once the
456
+ // IDP correctly withholds refresh_token until post-2FA.
432
457
  const hasRefreshToken = !!result.refresh_token;
433
458
  const requiresTwoFactor = result.user?.requiresTwoFactor ?? result.requiresTwoFactor ?? !hasRefreshToken;
434
459
  const idpTokenData = {
@@ -442,31 +467,125 @@ async function exchangeOAuthForIdpTokens(sessionToken, provider = 'google') {
442
467
  name: result.user?.full_name || result.user?.name || result.name || name,
443
468
  image: image,
444
469
  roles: result.user?.roles || result.roles || [],
445
- mfaVerified: !requiresTwoFactor,
470
+ mfaVerified: false, // Always false post-OAuth — see IDP-2FA-BUG note above
471
+ // Cards 141688/141741 — RELAYED, NOT DERIVED.
472
+ //
473
+ // The IDP already decides this (ExternalAuthController:943 and :2024,
474
+ // WO 113023): "enroll_mfa" = no CONFIRMED method, "mfa_verify" = a
475
+ // confirmed method exists, null = fully authenticated. It was emitted
476
+ // here and dropped on this line — `idpTokenData` is built field-by-field,
477
+ // so anything not named is silently discarded. Both downstream consumers
478
+ // (the middleware gate and the verify-code page) then reasoned in its
479
+ // absence and sent zero-method users to a page they cannot act on.
480
+ //
481
+ // WIRE NAME IS SNAKE: `next_step`, per [JsonPropertyName("next_step")] on
482
+ // BOTH OAuthCallbackResponse and ExternalTokenResponse. Reading
483
+ // `result.nextStep` here would be `undefined` forever and fail silently —
484
+ // the same camel/snake break `requires_two_factor` already cost us once.
485
+ //
486
+ // Do NOT recompute this from a method query. The IDP owns the fact; it is
487
+ // already usable-aware (confirmed and not revoked, per DotNetPert), which
488
+ // is the distinction that decides correctness: a revoked-TOTP user has
489
+ // count>0 but usable=0, and deriving it locally would let through exactly
490
+ // the users we must challenge.
491
+ // No `?? null`: if the response has no `next_step` at all (older IDP
492
+ // build, or a path that does not set it), that is UNKNOWN, not "fully
493
+ // authenticated". Defaulting it to null here would manufacture a
494
+ // clean-bill-of-health the IDP never gave and fail open at the gate.
495
+ nextStep: result.next_step,
446
496
  idpClientId: result.client_id ? String(result.client_id) : undefined,
447
497
  merchantId: result.merchant_id ? String(result.merchant_id) : undefined,
448
498
  };
449
- // Store in BA Redis session (for decodeSession)
499
+ // Storage is shared with the passkey sign-in path — see storeIdpTokensInSession.
500
+ const stored = await storeIdpTokensInSession(sessionToken, idpTokenData, provider);
501
+ if (stored)
502
+ console.log('[BETTER_AUTH] IDP tokens stored in session for', email);
503
+ return stored;
504
+ }
505
+ catch (err) {
506
+ console.error('[BETTER_AUTH] IDP token exchange failed:', err instanceof Error ? err.message : String(err));
507
+ return false;
508
+ }
509
+ }
510
+ /**
511
+ * Write IDP tokens into an EXISTING Better Auth session. Storage only — this
512
+ * function never acquires tokens and never decides who the caller is.
513
+ *
514
+ * ── WHY THIS IS SPLIT OUT (BAPert ruling, 2026-08-04) ────────────────────────
515
+ *
516
+ * `exchangeOAuthForIdpTokens` above does TWO things: it ACQUIRES tokens by
517
+ * POSTing to `/api/ExternalAuth/oauth-callback`, and then it STORES them. Only
518
+ * the second half is reusable, and the first half is actively dangerous to
519
+ * reuse:
520
+ *
521
+ * oauth-callback mints an IDP bearer from an ASSERTED EMAIL with no
522
+ * cryptographic verification of any kind (card 138635).
523
+ *
524
+ * Passkey sign-in already holds something strictly better — a grant issued by
525
+ * `/api/ExternalAuth/passwordless/passkey/verify`, which checked a real
526
+ * signature against a registered credential. Routing that through
527
+ * oauth-callback to obtain a session would DOWNGRADE a cryptographically
528
+ * verified assertion into an unverified one, make passkey sign-in weaker than
529
+ * its own ceremony, and inherit 138635 wholesale.
530
+ *
531
+ * That is not hypothetical: "do what the OAuth path does" is the obvious
532
+ * implementation and it is the wrong one. Splitting acquisition from storage is
533
+ * what makes the wrong one hard to write by accident.
534
+ *
535
+ * ── WHAT CALLERS MUST HAVE DONE FIRST ───────────────────────────────────────
536
+ *
537
+ * A Better Auth session must ALREADY EXIST for `sessionToken`. This function
538
+ * enriches; it does not create. Better Auth owns session creation and cookie
539
+ * signing, and nothing in this package should replicate either — the cookie is
540
+ * `{token}.{base64(hmac)}` with standard padding, signed with the runtime
541
+ * `authSecret` from Redis, and hand-rolling that produces a session which
542
+ * decodes but does not authenticate.
543
+ *
544
+ * @param sessionToken the Better Auth session token (from its Set-Cookie)
545
+ * @param idpTokenData tokens + identity, already obtained by the CALLER'S OWN
546
+ * verified route. Built field-by-field by the caller so an
547
+ * unnamed field cannot ride along silently.
548
+ * @param authProvider provenance label recorded on the canonical row. Written
549
+ * to the historical `oauthProvider` key, which predates
550
+ * non-OAuth callers; the NAME is legacy, the VALUE should
551
+ * state what actually authenticated ('passkey', 'google').
552
+ */
553
+ async function storeIdpTokensInSession(sessionToken, idpTokenData, authProvider) {
554
+ try {
555
+ const config = await (0, idp_client_config_1.getIDPClientConfig)();
556
+ const appSlug = config.clientSlug || (0, app_slug_1.getAppSlug)();
557
+ const baKey = `ba:${appSlug}:${sessionToken}`;
558
+ const baRaw = await (0, redis_1.getRedis)().get(baKey).catch(() => null);
559
+ if (!baRaw) {
560
+ // No session to enrich. Do NOT create one as a fallback: a session
561
+ // conjured here would carry no Better Auth cookie, so it would be
562
+ // unreachable by the browser and invisible to every other consumer —
563
+ // failing as "silently signed out" rather than as an error.
564
+ console.warn('[BETTER_AUTH] storeIdpTokensInSession: no BA session for token', sessionToken.substring(0, 10));
565
+ return false;
566
+ }
567
+ const baData = JSON.parse(baRaw);
450
568
  baData.idpTokens = idpTokenData;
451
- await (0, redis_1.getRedis)().setex(baKey, 7 * 24 * 60 * 60, JSON.stringify(baData));
452
- // Write to canonical session store so refresh handler and token lifecycle can find the tokens.
453
- // Key format: {sessionPrefix}{token} — same key that getSession() reads from.
569
+ await (0, redis_1.getRedis)().setex(baKey, session_store_1.SESSION_TTL, JSON.stringify(baData));
570
+ // Canonical store — the key getSession() reads, used by the refresh handler
571
+ // and token lifecycle. A failure here leaves the BA session correct and the
572
+ // canonical row missing, so it is warned and not fatal: the user is signed
573
+ // in, and refresh degrades rather than the sign-in failing outright.
454
574
  try {
455
575
  const { getSessionPrefix } = await Promise.resolve().then(() => __importStar(require('../lib/app-slug')));
456
576
  const canonicalKey = `${getSessionPrefix()}${sessionToken}`;
457
- await (0, redis_1.getRedis)().setex(canonicalKey, 7 * 24 * 60 * 60, JSON.stringify({
577
+ await (0, redis_1.getRedis)().setex(canonicalKey, session_store_1.SESSION_TTL, JSON.stringify({
458
578
  ...idpTokenData,
459
- oauthProvider: provider,
579
+ oauthProvider: authProvider,
460
580
  }));
461
581
  }
462
582
  catch (canonicalErr) {
463
583
  console.warn('[BETTER_AUTH] Failed to write canonical session:', canonicalErr instanceof Error ? canonicalErr.message : String(canonicalErr));
464
584
  }
465
- console.log('[BETTER_AUTH] IDP tokens stored in session for', email);
466
585
  return true;
467
586
  }
468
587
  catch (err) {
469
- console.error('[BETTER_AUTH] IDP token exchange failed:', err instanceof Error ? err.message : String(err));
588
+ console.error('[BETTER_AUTH] storeIdpTokensInSession failed:', err instanceof Error ? err.message : String(err));
470
589
  return false;
471
590
  }
472
591
  }
@@ -11,7 +11,7 @@
11
11
  * @version 1.0.0
12
12
  * @since auth-refactor-2026-01
13
13
  */
14
- import type { IdpLoginResponse, IdpOAuthCallbackResponse, IdpRefreshResponse, LoginCredentials } from '../types/auth-types';
14
+ import type { IdpLoginResponse, IdpRefreshResponse, LoginCredentials } from '../types/auth-types';
15
15
  /**
16
16
  * Get IDP base URL. Throws if not configured.
17
17
  */
@@ -32,30 +32,21 @@ export declare function idpLogin(credentials: LoginCredentials, clientHeaders?:
32
32
  userAgent?: string;
33
33
  }): Promise<IdpLoginResponse>;
34
34
  /**
35
- * Register/authenticate OAuth user with IDP.
35
+ * idpOAuthCallback REMOVED 2026-08-05 (NextPert, BAPert 20168 / QAPert 20166).
36
36
  *
37
- * Called after OAuth provider (Google, etc.) redirects back.
38
- * Creates or retrieves IDP user and returns IDP tokens.
37
+ * It POSTed to /api/ExternalAuth/oauth-callback with provider, email, name,
38
+ * tokens and client_id — ALL ASSERTIONS, no id_token, nothing verifiable. After
39
+ * the 170003 fix (13436d56e) that call can only ever return 401.
39
40
  *
40
- * @param oauthData - Data from OAuth provider
41
- * @returns IDP response with tokens and user info
42
- */
43
- export declare function idpOAuthCallback(oauthData: {
44
- provider: string;
45
- providerAccountId: string;
46
- email: string;
47
- name?: string;
48
- image?: string;
49
- accessToken?: string;
50
- refreshToken?: string;
51
- expiresAt?: number;
52
- }): Promise<IdpOAuthCallbackResponse>;
53
- /**
54
- * Refresh an expired access token using the refresh token.
41
+ * It was a SECOND caller of the same endpoint, and only the first
42
+ * (better-auth.ts, card 138635) was hardened — the same duplicated-integration
43
+ * trap the viability code documents ("adding the relay to just one would leave
44
+ * an app that mounts THIS one still blind").
55
45
  *
56
- * @param refreshToken - The refresh token from previous login
57
- * @param mfaContext - MFA context to preserve across refresh
58
- * @returns New tokens or error
46
+ * It had no call sites and was not reachable through the package exports map
47
+ * (no ./auth/utils key, no wildcard), so removing it breaks nothing. Deleted
48
+ * rather than fixed, because an unreachable function whose only outcome is a
49
+ * 401 that LOOKS like a security success is a trap for whoever finds it next.
59
50
  */
60
51
  export declare function idpRefreshToken(refreshToken: string, mfaContext?: {
61
52
  amr?: string[];
@@ -16,7 +16,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.getIdpUrl = getIdpUrl;
17
17
  exports.getClientId = getClientId;
18
18
  exports.idpLogin = idpLogin;
19
- exports.idpOAuthCallback = idpOAuthCallback;
20
19
  exports.idpRefreshToken = idpRefreshToken;
21
20
  exports.idpVerify2FA = idpVerify2FA;
22
21
  exports.idpSend2FACode = idpSend2FACode;
@@ -133,90 +132,21 @@ function getLoginErrorMessage(status, responseData) {
133
132
  // OAUTH CALLBACK
134
133
  // ============================================================================
135
134
  /**
136
- * Register/authenticate OAuth user with IDP.
135
+ * idpOAuthCallback REMOVED 2026-08-05 (NextPert, BAPert 20168 / QAPert 20166).
137
136
  *
138
- * Called after OAuth provider (Google, etc.) redirects back.
139
- * Creates or retrieves IDP user and returns IDP tokens.
137
+ * It POSTed to /api/ExternalAuth/oauth-callback with provider, email, name,
138
+ * tokens and client_id — ALL ASSERTIONS, no id_token, nothing verifiable. After
139
+ * the 170003 fix (13436d56e) that call can only ever return 401.
140
140
  *
141
- * @param oauthData - Data from OAuth provider
142
- * @returns IDP response with tokens and user info
143
- */
144
- async function idpOAuthCallback(oauthData) {
145
- const idpUrl = getIdpUrl();
146
- const clientId = getClientId();
147
- try {
148
- const response = await fetch(`${idpUrl}/api/ExternalAuth/oauth-callback`, {
149
- method: 'POST',
150
- headers: {
151
- 'Content-Type': 'application/json',
152
- 'X-Client-Id': clientId,
153
- },
154
- body: JSON.stringify({
155
- provider: oauthData.provider,
156
- provider_account_id: oauthData.providerAccountId,
157
- email: oauthData.email,
158
- name: oauthData.name || '',
159
- image: oauthData.image || '',
160
- access_token: oauthData.accessToken || '',
161
- refresh_token: oauthData.refreshToken || '',
162
- expires_at: oauthData.expiresAt || 0,
163
- client_id: clientId,
164
- }),
165
- });
166
- if (!response.ok) {
167
- const errorText = await response.text().catch(() => 'Unknown error');
168
- console.error('[IDP_CLIENT] OAuth callback failed:', response.status, errorText);
169
- return {
170
- success: false,
171
- error: {
172
- code: `HTTP_${response.status}`,
173
- message: 'OAuth registration failed',
174
- },
175
- };
176
- }
177
- const data = await response.json();
178
- const responseData = data.data || data;
179
- // Normalize snake_case to camelCase
180
- return {
181
- success: responseData.success !== false,
182
- data: responseData.success !== false
183
- ? {
184
- accessToken: responseData.accessToken || responseData.access_token,
185
- refreshToken: responseData.refreshToken || responseData.refresh_token,
186
- isNewUser: responseData.isNewUser ?? responseData.is_new_user ?? false,
187
- user: responseData.user
188
- ? {
189
- userId: responseData.user.userId || responseData.user.user_id,
190
- email: responseData.user.email || responseData.user.Email,
191
- fullName: responseData.user.fullName || responseData.user.full_name || responseData.user.name,
192
- roles: responseData.user.roles || [],
193
- }
194
- : undefined,
195
- }
196
- : undefined,
197
- error: responseData.error,
198
- };
199
- }
200
- catch (error) {
201
- console.error('[IDP_CLIENT] OAuth callback request failed:', error);
202
- return {
203
- success: false,
204
- error: {
205
- code: 'NETWORK_ERROR',
206
- message: 'Failed to connect to authentication service',
207
- },
208
- };
209
- }
210
- }
211
- // ============================================================================
212
- // TOKEN REFRESH
213
- // ============================================================================
214
- /**
215
- * Refresh an expired access token using the refresh token.
141
+ * It was a SECOND caller of the same endpoint, and only the first
142
+ * (better-auth.ts, card 138635) was hardened — the same duplicated-integration
143
+ * trap the viability code documents ("adding the relay to just one would leave
144
+ * an app that mounts THIS one still blind").
216
145
  *
217
- * @param refreshToken - The refresh token from previous login
218
- * @param mfaContext - MFA context to preserve across refresh
219
- * @returns New tokens or error
146
+ * It had no call sites and was not reachable through the package exports map
147
+ * (no ./auth/utils key, no wildcard), so removing it breaks nothing. Deleted
148
+ * rather than fixed, because an unreachable function whose only outcome is a
149
+ * 401 that LOOKS like a security success is a trap for whoever finds it next.
220
150
  */
221
151
  async function idpRefreshToken(refreshToken, mfaContext) {
222
152
  const idpUrl = getIdpUrl();