@payez/next-mvp 4.4.5 → 4.4.7
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/README.md +10 -3
- package/dist/api/auth-handler.js +5 -3
- package/dist/api-handlers/account/change-password.js +33 -8
- package/dist/api-handlers/account/masked-info.js +2 -1
- package/dist/api-handlers/account/recovery/forward.d.ts +14 -0
- package/dist/api-handlers/account/recovery/forward.js +105 -0
- package/dist/api-handlers/account/recovery/initiate.js +4 -0
- package/dist/api-handlers/account/recovery/send-code.d.ts +12 -2
- package/dist/api-handlers/account/recovery/send-code.js +11 -84
- package/dist/api-handlers/account/recovery/verify-code.d.ts +11 -2
- package/dist/api-handlers/account/recovery/verify-code.js +10 -84
- package/dist/api-handlers/account/reset-password.js +24 -1
- package/dist/api-handlers/account/send-code.d.ts +1 -15
- package/dist/api-handlers/account/send-code.js +3 -6
- package/dist/api-handlers/account/update-phone.js +6 -1
- package/dist/api-handlers/account/validate-password.js +3 -1
- package/dist/api-handlers/account/verify-email.d.ts +1 -17
- package/dist/api-handlers/account/verify-email.js +4 -6
- package/dist/api-handlers/account/verify-sms.d.ts +1 -17
- package/dist/api-handlers/account/verify-sms.js +4 -6
- package/dist/api-handlers/admin/analytics.js +12 -4
- package/dist/api-handlers/admin/audit.js +12 -4
- package/dist/api-handlers/admin/redis-sessions.js +16 -8
- package/dist/api-handlers/admin/sessions.js +14 -6
- package/dist/api-handlers/admin/site-logs.js +39 -16
- package/dist/api-handlers/admin/stats.js +14 -4
- package/dist/api-handlers/admin/users.js +14 -6
- package/dist/api-handlers/admin/vibe-data.js +19 -11
- package/dist/api-handlers/auth/jwks.js +2 -1
- package/dist/api-handlers/auth/login.js +13 -4
- package/dist/api-handlers/auth/passkey-signin.js +6 -0
- package/dist/api-handlers/auth/refresh.js +38 -11
- package/dist/api-handlers/auth/signout.js +2 -1
- package/dist/api-handlers/auth/status.js +4 -1
- package/dist/api-handlers/auth/update-session.d.ts +25 -4
- package/dist/api-handlers/auth/update-session.js +38 -11
- package/dist/api-handlers/auth/validate.js +4 -2
- package/dist/api-handlers/auth/verify-code.js +3 -2
- package/dist/api-handlers/mfa/backup-code-verify.d.ts +1 -18
- package/dist/api-handlers/mfa/backup-code-verify.js +4 -6
- package/dist/api-handlers/mfa/backup-codes.js +2 -1
- package/dist/api-handlers/mfa/methods.js +2 -1
- package/dist/api-handlers/mfa/totp-confirm.js +2 -1
- package/dist/api-handlers/mfa/totp-enroll.js +2 -1
- package/dist/api-handlers/mfa/totp-verify.d.ts +1 -19
- package/dist/api-handlers/mfa/totp-verify.js +4 -6
- package/dist/api-handlers/passkeys/list.js +2 -1
- package/dist/api-handlers/passkeys/manage.js +2 -1
- package/dist/api-handlers/passkeys/register-options.js +2 -1
- package/dist/api-handlers/passkeys/register-verify.js +2 -1
- package/dist/api-handlers/session/refresh-viability.d.ts +4 -2
- package/dist/api-handlers/session/refresh-viability.js +10 -7
- package/dist/api-handlers/session/viability.js +4 -3
- package/dist/api-handlers/test/force-expire.js +5 -3
- package/dist/auth/auth-decision.js +2 -1
- package/dist/auth/better-auth.d.ts +58 -14
- package/dist/auth/better-auth.js +125 -54
- package/dist/auth/utils/idp-client.js +2 -1
- package/dist/client/better-auth-client.d.ts +101 -956
- package/dist/components/admin/DataBrowserTab.js +4 -1
- package/dist/components/admin/SessionsTab.js +9 -3
- package/dist/components/recovery/RequestedStep.d.ts +11 -0
- package/dist/components/recovery/RequestedStep.js +14 -0
- package/dist/components/recovery/SetPasswordStep.js +10 -1
- package/dist/config/env.d.ts +0 -2
- package/dist/config/env.js +0 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -1
- package/dist/lib/app-slug.d.ts +21 -0
- package/dist/lib/app-slug.js +51 -0
- package/dist/lib/ensure-fresh-access-token.d.ts +26 -0
- package/dist/lib/ensure-fresh-access-token.js +105 -4
- package/dist/lib/idp-client-config.d.ts +20 -0
- package/dist/lib/idp-client-config.js +73 -45
- package/dist/lib/idp-fetch.js +1 -1
- package/dist/lib/never-in-response.d.ts +30 -0
- package/dist/lib/never-in-response.js +71 -0
- package/dist/lib/password-rules.d.ts +22 -0
- package/dist/lib/password-rules.js +43 -0
- package/dist/lib/safe-error-response.d.ts +67 -0
- package/dist/lib/safe-error-response.js +130 -0
- package/dist/lib/session-cookie-shape.d.ts +22 -19
- package/dist/lib/session-cookie-shape.js +14 -18
- package/dist/lib/session-store.d.ts +8 -1
- package/dist/lib/session-store.js +170 -24
- package/dist/lib/startup-init.d.ts +6 -0
- package/dist/lib/startup-init.js +30 -4
- package/dist/lib/token-lifecycle.js +2 -1
- package/dist/lib/token-log-id.d.ts +18 -0
- package/dist/lib/token-log-id.js +84 -0
- package/dist/lib/two-factor-verify-failure.d.ts +78 -0
- package/dist/lib/two-factor-verify-failure.js +220 -0
- package/dist/middleware/create-middleware.d.ts +4 -1
- package/dist/middleware/create-middleware.js +79 -8
- package/dist/pages/login/page.js +5 -0
- package/dist/pages/recovery/page.js +14 -4
- package/dist/pages/test-env/JwtInspectPage.js +7 -149
- package/dist/pages/test-env/RefreshTokenPage.js +4 -1
- package/dist/pages/test-env/redact-credentials.d.ts +6 -0
- package/dist/pages/test-env/redact-credentials.js +101 -0
- package/dist/pages/verify-code/page.js +6 -3
- package/dist/routes/auth/logout.d.ts +2 -1
- package/dist/routes/auth/logout.js +4 -2
- package/dist/routes/auth/session.d.ts +0 -3
- package/dist/routes/auth/session.js +16 -10
- package/dist/routes/auth/settings.js +4 -1
- package/dist/routes/auth/viability.d.ts +3 -2
- package/dist/routes/auth/viability.js +11 -7
- package/dist/server/auth.d.ts +36 -294
- package/dist/server/auth.js +32 -31
- package/dist/server/decode-session.js +15 -26
- package/dist/server/verified-session-token.d.ts +13 -0
- package/dist/server/verified-session-token.js +103 -0
- package/dist/stores/authStore.js +14 -3
- package/dist/types/recovery.d.ts +1 -1
- package/dist/utils/api.d.ts +18 -0
- package/dist/utils/api.js +51 -9
- package/dist/vibe/client.d.ts +132 -16
- package/dist/vibe/client.js +288 -82
- package/dist/vibe/document-unwrap.d.ts +50 -0
- package/dist/vibe/document-unwrap.js +78 -0
- package/dist/vibe/errors.d.ts +17 -0
- package/dist/vibe/errors.js +21 -1
- package/dist/vibe/generic.d.ts +2 -49
- package/dist/vibe/generic.js +17 -78
- package/dist/vibe/index.d.ts +1 -1
- package/dist/vibe/index.js +2 -1
- package/dist/vibe/sessions.js +2 -1
- package/package.json +13 -2
- package/scripts/verify-release-provenance.mjs +410 -0
- package/src/api/auth-handler.ts +5 -3
- package/src/api-handlers/account/change-password.ts +36 -8
- package/src/api-handlers/account/masked-info.ts +2 -1
- package/src/api-handlers/account/recovery/forward.ts +117 -0
- package/src/api-handlers/account/recovery/initiate.ts +3 -0
- package/src/api-handlers/account/recovery/send-code.ts +13 -99
- package/src/api-handlers/account/recovery/verify-code.ts +12 -99
- package/src/api-handlers/account/reset-password.ts +30 -1
- package/src/api-handlers/account/send-code.ts +3 -9
- package/src/api-handlers/account/update-phone.ts +6 -1
- package/src/api-handlers/account/validate-password.ts +3 -1
- package/src/api-handlers/account/verify-email.ts +4 -9
- package/src/api-handlers/account/verify-sms.ts +4 -9
- package/src/api-handlers/admin/analytics.ts +13 -5
- package/src/api-handlers/admin/audit.ts +13 -5
- package/src/api-handlers/admin/redis-sessions.ts +16 -8
- package/src/api-handlers/admin/sessions.ts +15 -7
- package/src/api-handlers/admin/site-logs.ts +39 -16
- package/src/api-handlers/admin/stats.ts +15 -5
- package/src/api-handlers/admin/users.ts +15 -7
- package/src/api-handlers/admin/vibe-data.ts +20 -12
- package/src/api-handlers/auth/jwks.ts +2 -1
- package/src/api-handlers/auth/login.ts +13 -4
- package/src/api-handlers/auth/passkey-signin.ts +4 -0
- package/src/api-handlers/auth/refresh.ts +38 -11
- package/src/api-handlers/auth/signout.ts +2 -1
- package/src/api-handlers/auth/status.ts +4 -1
- package/src/api-handlers/auth/update-session.ts +45 -11
- package/src/api-handlers/auth/validate.ts +4 -2
- package/src/api-handlers/auth/verify-code.ts +3 -2
- package/src/api-handlers/mfa/backup-code-verify.ts +4 -9
- package/src/api-handlers/mfa/backup-codes.ts +2 -1
- package/src/api-handlers/mfa/methods.ts +2 -1
- package/src/api-handlers/mfa/totp-confirm.ts +2 -1
- package/src/api-handlers/mfa/totp-enroll.ts +2 -1
- package/src/api-handlers/mfa/totp-verify.ts +4 -9
- package/src/api-handlers/passkeys/list.ts +2 -1
- package/src/api-handlers/passkeys/manage.ts +2 -1
- package/src/api-handlers/passkeys/register-options.ts +2 -1
- package/src/api-handlers/passkeys/register-verify.ts +2 -1
- package/src/api-handlers/session/refresh-viability.ts +10 -7
- package/src/api-handlers/session/viability.ts +4 -3
- package/src/api-handlers/test/force-expire.ts +5 -3
- package/src/auth/auth-decision.ts +2 -1
- package/src/auth/better-auth.ts +128 -61
- package/src/auth/utils/idp-client.ts +2 -1
- package/src/components/admin/DataBrowserTab.tsx +4 -1
- package/src/components/admin/SessionsTab.tsx +10 -3
- package/src/components/recovery/RequestedStep.tsx +30 -0
- package/src/components/recovery/SetPasswordStep.tsx +24 -3
- package/src/config/env.ts +0 -2
- package/src/hooks/useViabilitySession.ts +2 -1
- package/src/index.ts +4 -0
- package/src/lib/app-slug.ts +50 -0
- package/src/lib/ensure-fresh-access-token.ts +133 -4
- package/src/lib/idp-client-config.ts +98 -45
- package/src/lib/idp-fetch.ts +1 -1
- package/src/lib/never-in-response.ts +64 -0
- package/src/lib/password-rules.ts +50 -0
- package/src/lib/safe-error-response.ts +126 -0
- package/src/lib/session-cookie-shape.ts +23 -22
- package/src/lib/session-store.ts +167 -24
- package/src/lib/startup-init.ts +29 -4
- package/src/lib/token-lifecycle.ts +2 -1
- package/src/lib/token-log-id.ts +69 -0
- package/src/lib/two-factor-verify-failure.ts +248 -0
- package/src/middleware/create-middleware.ts +97 -9
- package/src/pages/login/page.tsx +4 -0
- package/src/pages/recovery/page.tsx +21 -4
- package/src/pages/test-env/JwtInspectPage.tsx +11 -233
- package/src/pages/test-env/RefreshTokenPage.tsx +9 -4
- package/src/pages/test-env/redact-credentials.ts +96 -0
- package/src/pages/verify-code/page.tsx +6 -3
- package/src/routes/auth/logout.ts +4 -2
- package/src/routes/auth/session.ts +16 -10
- package/src/routes/auth/settings.ts +5 -1
- package/src/routes/auth/viability.ts +11 -7
- package/src/server/auth.ts +33 -35
- package/src/server/decode-session.ts +16 -26
- package/src/server/verified-session-token.ts +103 -0
- package/src/stores/authStore.ts +16 -5
- package/src/types/recovery.ts +1 -1
- package/src/utils/api.ts +47 -8
- package/src/vibe/client.ts +378 -91
- package/src/vibe/document-unwrap.ts +103 -0
- package/src/vibe/errors.ts +24 -0
- package/src/vibe/generic.ts +8 -100
- package/src/vibe/index.ts +1 -0
- package/src/vibe/sessions.ts +2 -1
package/src/auth/better-auth.ts
CHANGED
|
@@ -21,9 +21,11 @@ import { resolveClientKey } from '../lib/client-key';
|
|
|
21
21
|
import { genericOAuth } from 'better-auth/plugins/generic-oauth';
|
|
22
22
|
import type { IDPClientConfig } from '../lib/idp-client-config';
|
|
23
23
|
import { getIDPClientConfig } from '../lib/idp-client-config';
|
|
24
|
-
import { getAppSlug } from '../lib/app-slug';
|
|
24
|
+
import { getAppSlug, assertAppSlugMatchesIdp } from '../lib/app-slug';
|
|
25
25
|
import { getRedis } from '../lib/redis';
|
|
26
|
+
import { createSingleFlight, type SingleFlight } from '../lib/single-flight';
|
|
26
27
|
import { SESSION_TTL } from '../lib/session-store';
|
|
28
|
+
import { tokenLogId } from '../lib/token-log-id';
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* 185121: short sha256 fingerprint for secrets/tokens in log lines — enough
|
|
@@ -518,6 +520,7 @@ export function createBetterAuthInstance(
|
|
|
518
520
|
idpConfig: IDPClientConfig,
|
|
519
521
|
opts: CreateBetterAuthInstanceOptions = {}
|
|
520
522
|
) {
|
|
523
|
+
assertAppSlugMatchesIdp(idpConfig.clientSlug);
|
|
521
524
|
const appSlug = idpConfig.clientSlug || getAppSlug();
|
|
522
525
|
|
|
523
526
|
// 185121 line 1: name the secret this instance PINNED, as a hash, never the
|
|
@@ -817,6 +820,19 @@ export function createBetterAuthInstance(
|
|
|
817
820
|
},
|
|
818
821
|
},
|
|
819
822
|
|
|
823
|
+
// PAY-1781 B: KEEP OAUTH STATE IN THE ENCRYPTED COOKIE, as it was on better-auth 1.5.6.
|
|
824
|
+
// 1.5.6 resolved the default as `options.database ? 'database' : 'cookie'` - we have no
|
|
825
|
+
// database, so: cookie. 1.6.13+ resolves it as `(database || secondaryStorage) ? 'database'
|
|
826
|
+
// : 'cookie'`, and we DO have secondaryStorage, so the upgrade alone would silently move every
|
|
827
|
+
// OAuth state record into Redis through the adapter above (its enrichment read-modify-write
|
|
828
|
+
// and key handling were built for session records, not state). Pinned so the upgrade changes
|
|
829
|
+
// the version, not the design. The cookie branch is where GHSA-wxw3-q3m9-c3jr lived; 1.6.2+
|
|
830
|
+
// compares the returned `state` with the nonce sealed in the cookie, and our provider is also
|
|
831
|
+
// pkce:true, which the advisory required to be false. Test: oauth-state-strategy-cookie.
|
|
832
|
+
account: {
|
|
833
|
+
storeStateStrategy: 'cookie',
|
|
834
|
+
},
|
|
835
|
+
|
|
820
836
|
// Cookie prefix must match slim-middleware expectations ({slug}.session-token)
|
|
821
837
|
advanced: {
|
|
822
838
|
cookiePrefix: appSlug,
|
|
@@ -867,6 +883,9 @@ interface BetterAuthModuleState {
|
|
|
867
883
|
cachedInstance: any;
|
|
868
884
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
869
885
|
initPromise: Promise<any> | null;
|
|
886
|
+
// PAY-642: the init attempt, single-flighted so a REJECTED attempt clears itself (see getBetterAuthInstance).
|
|
887
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
888
|
+
initFlight?: SingleFlight<any>;
|
|
870
889
|
configuredOpts: CreateBetterAuthInstanceOptions;
|
|
871
890
|
}
|
|
872
891
|
|
|
@@ -902,19 +921,36 @@ export function configureBetterAuth(opts: CreateBetterAuthInstanceOptions): void
|
|
|
902
921
|
state.configuredOpts = opts;
|
|
903
922
|
}
|
|
904
923
|
|
|
924
|
+
/**
|
|
925
|
+
* PAY-642 (card 338215): the init attempt no longer LATCHES A FAILURE.
|
|
926
|
+
*
|
|
927
|
+
* This used to be `state.initPromise ??= getIDPClientConfig(true).then(...)` with no .catch and no reset. One
|
|
928
|
+
* auth request against a cold process while the IdP was unreachable stored a rejected promise, and every later
|
|
929
|
+
* caller replayed it until the process restarted, even after the IdP recovered. This funnel backs every
|
|
930
|
+
* /api/auth/* route, the login GET handler and withAuth. server/auth.ts getAuthInstance() already used
|
|
931
|
+
* createSingleFlight for this exact bug; this sibling funnel was missed.
|
|
932
|
+
*
|
|
933
|
+
* Now the attempt runs through the same helper: concurrent callers share one attempt, a failure still rejects
|
|
934
|
+
* for every caller of THAT attempt (loud), the next call starts a fresh attempt, and a success is cached in
|
|
935
|
+
* state.cachedInstance as before. The flight lives on the globalThis state for the same reason the instance
|
|
936
|
+
* does (duplicated route chunks must share one init).
|
|
937
|
+
*/
|
|
905
938
|
export async function getBetterAuthInstance() {
|
|
906
939
|
if (state.cachedInstance) return state.cachedInstance;
|
|
907
940
|
|
|
908
|
-
|
|
909
|
-
|
|
941
|
+
state.initFlight ??= createSingleFlight(() =>
|
|
942
|
+
getIDPClientConfig(true).then(config => {
|
|
910
943
|
const instance = createBetterAuthInstance(config, state.configuredOpts);
|
|
911
944
|
state.cachedInstance = instance;
|
|
912
945
|
console.log('[BETTER_AUTH] Instance created for', config.clientSlug || config.clientId);
|
|
913
946
|
return instance;
|
|
914
|
-
})
|
|
915
|
-
|
|
947
|
+
})
|
|
948
|
+
);
|
|
916
949
|
|
|
917
|
-
|
|
950
|
+
const attempt = state.initFlight.get();
|
|
951
|
+
// Kept for callers that inspect it; it always points at the CURRENT attempt, never a settled failure.
|
|
952
|
+
state.initPromise = attempt;
|
|
953
|
+
return attempt;
|
|
918
954
|
}
|
|
919
955
|
|
|
920
956
|
/**
|
|
@@ -961,7 +997,7 @@ export async function exchangeOAuthForIdpTokens(
|
|
|
961
997
|
// Read the BA session from Redis
|
|
962
998
|
const baRaw = await getRedis().get(baKey).catch(() => null);
|
|
963
999
|
if (!baRaw) {
|
|
964
|
-
console.warn('[BETTER_AUTH] exchangeOAuthForIdpTokens: session not found in Redis for token', sessionToken
|
|
1000
|
+
console.warn('[BETTER_AUTH] exchangeOAuthForIdpTokens: session not found in Redis for token', tokenLogId(sessionToken));
|
|
965
1001
|
return false;
|
|
966
1002
|
}
|
|
967
1003
|
|
|
@@ -1038,7 +1074,9 @@ export async function exchangeOAuthForIdpTokens(
|
|
|
1038
1074
|
});
|
|
1039
1075
|
|
|
1040
1076
|
const oauthResText = await oauthRes.text();
|
|
1041
|
-
|
|
1077
|
+
// PAY-1427 (QAPert, PR 154 thread 326): this printed the first 500 chars of the oauth-callback TOKEN
|
|
1078
|
+
// envelope on EVERY successful OAuth login (it runs before the !ok check). Status + length only.
|
|
1079
|
+
console.log('[BETTER_AUTH] IDP oauth-callback response:', { status: oauthRes.status, bodyLength: oauthResText.length });
|
|
1042
1080
|
|
|
1043
1081
|
if (!oauthRes.ok) {
|
|
1044
1082
|
console.error('[BETTER_AUTH] IDP oauth-callback failed:', oauthRes.status);
|
|
@@ -1276,7 +1314,7 @@ export async function storeIdpTokensInSession(
|
|
|
1276
1314
|
// conjured here would carry no Better Auth cookie, so it would be
|
|
1277
1315
|
// unreachable by the browser and invisible to every other consumer —
|
|
1278
1316
|
// failing as "silently signed out" rather than as an error.
|
|
1279
|
-
console.warn('[BETTER_AUTH] storeIdpTokensInSession: no BA session for token', sessionToken
|
|
1317
|
+
console.warn('[BETTER_AUTH] storeIdpTokensInSession: no BA session for token', tokenLogId(sessionToken));
|
|
1280
1318
|
return false;
|
|
1281
1319
|
}
|
|
1282
1320
|
|
|
@@ -1306,12 +1344,79 @@ export async function storeIdpTokensInSession(
|
|
|
1306
1344
|
}
|
|
1307
1345
|
}
|
|
1308
1346
|
|
|
1347
|
+
/**
|
|
1348
|
+
* The `?error=` code the login page receives when an OAuth sign-in is ABORTED because the IdP exchange did not
|
|
1349
|
+
* store tokens (PAY-1733). Hosts map it to ONE neutral line; the card's words are "That sign-in didn't work.
|
|
1350
|
+
* Try again or use another method." The cause stays in the server log, never on the page.
|
|
1351
|
+
*/
|
|
1352
|
+
export const OAUTH_SIGN_IN_FAILED_ERROR = 'OAUTH_SIGN_IN_FAILED';
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Re-emit each cookie the callback response SET as an immediate expiry, keeping the attributes that decide
|
|
1356
|
+
* WHICH cookie the browser overwrites (Path, Domain, Secure, SameSite, HttpOnly - a `__Secure-` name is only
|
|
1357
|
+
* accepted with Secure) and dropping Expires/Max-Age in favour of Max-Age=0.
|
|
1358
|
+
*/
|
|
1359
|
+
export function expireSetCookies(setCookies: readonly string[]): string[] {
|
|
1360
|
+
const out: string[] = [];
|
|
1361
|
+
for (const sc of setCookies) {
|
|
1362
|
+
const [pair, ...attrs] = sc.split(';').map((s) => s.trim());
|
|
1363
|
+
const eq = pair.indexOf('=');
|
|
1364
|
+
const name = eq > 0 ? pair.slice(0, eq) : '';
|
|
1365
|
+
if (!name) continue;
|
|
1366
|
+
const kept = attrs.filter((a) => /^(path|domain|secure|samesite|httponly)\b/i.test(a));
|
|
1367
|
+
out.push([`${name}=`, ...kept, 'Max-Age=0'].join('; '));
|
|
1368
|
+
}
|
|
1369
|
+
return out;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* PAY-1733 (Jon, live on :4797): ABORT an OAuth sign-in whose IdP exchange did not store tokens.
|
|
1374
|
+
*
|
|
1375
|
+
* Before this, a failed exchange was a console.error and the user kept the Better Auth session the callback had
|
|
1376
|
+
* just created: signed in, holding NO IdP tokens. The host then routed that session to 2FA, which painted the
|
|
1377
|
+
* user's email and failed every call - a red "sign in again" screen about an account the app could no longer
|
|
1378
|
+
* authenticate. The IdP's 401 was CORRECT (it fail-closes unverified assertions); keeping the session was the bug.
|
|
1379
|
+
*
|
|
1380
|
+
* So: no session without IdP tokens. The Better Auth session is destroyed (its own signOut, best effort, then the
|
|
1381
|
+
* `ba:<slug>:<token>` record directly - that record is what makes it resolvable), every cookie the callback set is
|
|
1382
|
+
* expired, and the user goes to the login page with OAUTH_SIGN_IN_FAILED. Relative Location, as for the state-error
|
|
1383
|
+
* branch above (328439: an absolute URL built from request.url is a dead tab in a container).
|
|
1384
|
+
*/
|
|
1385
|
+
async function abortOAuthSignIn(
|
|
1386
|
+
callbackResponse: Response,
|
|
1387
|
+
sessionToken: string | null,
|
|
1388
|
+
cookieHeader: string,
|
|
1389
|
+
loginPath: string,
|
|
1390
|
+
): Promise<Response> {
|
|
1391
|
+
const headers = new Headers();
|
|
1392
|
+
if (sessionToken) {
|
|
1393
|
+
try {
|
|
1394
|
+
const auth = await getBetterAuthInstance();
|
|
1395
|
+
const out = await auth?.api?.signOut?.({ headers: new Headers({ cookie: cookieHeader }), asResponse: true });
|
|
1396
|
+
for (const sc of (out as Response | undefined)?.headers?.getSetCookie?.() ?? []) headers.append('set-cookie', sc);
|
|
1397
|
+
} catch (err) {
|
|
1398
|
+
console.warn('[BETTER_AUTH] abort: Better Auth signOut failed, deleting the record directly:', err instanceof Error ? err.message : String(err));
|
|
1399
|
+
}
|
|
1400
|
+
try {
|
|
1401
|
+
const config = await getIDPClientConfig();
|
|
1402
|
+
await getRedis().del(`ba:${config.clientSlug || getAppSlug()}:${sessionToken}`);
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
console.error('[BETTER_AUTH] abort: could not delete the Better Auth session record', tokenLogId(sessionToken), err instanceof Error ? err.message : String(err));
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
for (const sc of expireSetCookies(callbackResponse.headers.getSetCookie?.() ?? [])) headers.append('set-cookie', sc);
|
|
1408
|
+
headers.set('location', `${loginPath}${loginPath.includes('?') ? '&' : '?'}error=${OAUTH_SIGN_IN_FAILED_ERROR}`);
|
|
1409
|
+
console.warn('[BETTER_AUTH] OAuth sign-in ABORTED: the IdP exchange stored no tokens, so no session is kept (PAY-1733)', sessionToken ? tokenLogId(sessionToken) : '(no session)');
|
|
1410
|
+
return new Response(null, { status: 302, headers });
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1309
1413
|
/**
|
|
1310
1414
|
* Create a production-ready GET handler for the auth catch-all route.
|
|
1311
1415
|
*
|
|
1312
1416
|
* Wraps better-auth's GET handler with:
|
|
1313
1417
|
* - OAuth state error recovery (redirects to login instead of error page)
|
|
1314
|
-
* - IDP token exchange after successful OAuth callback
|
|
1418
|
+
* - IDP token exchange after successful OAuth callback - and, when it stores no tokens, the sign-in is ABORTED
|
|
1419
|
+
* (session destroyed, cookies expired, login page with OAUTH_SIGN_IN_FAILED; PAY-1733)
|
|
1315
1420
|
*
|
|
1316
1421
|
* Usage in host app:
|
|
1317
1422
|
* ```ts
|
|
@@ -1383,67 +1488,28 @@ export function createAuthGetHandler(loginPath: string = '/account-auth/login')
|
|
|
1383
1488
|
}
|
|
1384
1489
|
}
|
|
1385
1490
|
|
|
1386
|
-
// After
|
|
1387
|
-
//
|
|
1388
|
-
//
|
|
1389
|
-
// site passed no provider, so it took exchangeOAuthForIdpTokens's
|
|
1390
|
-
// `provider: string = 'google'` default and asserted "google" to
|
|
1391
|
-
// /api/ExternalAuth/oauth-callback for EVERY provider. That was harmless
|
|
1392
|
-
// while Google was the only one wired, and became wrong the moment a second
|
|
1393
|
-
// provider shipped: a GitHub sign-in forwarded a GitHub access_token
|
|
1394
|
-
// labelled `provider: 'google'`, so the IDP tried to verify it as a Google
|
|
1395
|
-
// identity and fail-closed with 401 UNVERIFIED_ASSERTION in
|
|
1396
|
-
// `response_time_ms: 0` — it never had to call anyone to know it was wrong.
|
|
1397
|
-
//
|
|
1398
|
-
// The diagnostics made this maximally confusing: `access_token_forwarded:
|
|
1399
|
-
// true` is correct and the token really did arrive. The FIELD was fine; the
|
|
1400
|
-
// LABEL on it was a lie. Nothing on the client half is broken.
|
|
1401
|
-
//
|
|
1402
|
-
// The provider was never unavailable — it is the path segment this very
|
|
1403
|
-
// `if` matches on (/api/auth/callback/github). Derive it instead of
|
|
1404
|
-
// defaulting, because a default here cannot be right for more than one
|
|
1405
|
-
// provider at a time.
|
|
1491
|
+
// After the OAuth callback: exchange the provider identity for IDP tokens. If that does NOT store tokens - the
|
|
1492
|
+
// IdP refused (401), no session was found, or anything threw - the sign-in is ABORTED (PAY-1733). A session is
|
|
1493
|
+
// kept only when it holds IdP tokens.
|
|
1406
1494
|
const url = new URL(request.url);
|
|
1407
1495
|
if (url.pathname.includes('/callback/') && response.status === 302) {
|
|
1496
|
+
const setCookies = response.headers.getSetCookie?.() || [];
|
|
1497
|
+
const cookieHeader = setCookies
|
|
1498
|
+
.map((c: string) => c.split(';')[0])
|
|
1499
|
+
.join('; ');
|
|
1500
|
+
let sessionToken: string | null = null;
|
|
1501
|
+
let exchanged = false;
|
|
1408
1502
|
try {
|
|
1409
1503
|
const auth = await getBetterAuthInstance();
|
|
1410
1504
|
if (auth?.api?.getSession) {
|
|
1411
|
-
const setCookies = response.headers.getSetCookie?.() || [];
|
|
1412
|
-
const cookieHeader = setCookies
|
|
1413
|
-
.map((c: string) => c.split(';')[0])
|
|
1414
|
-
.join('; ');
|
|
1415
|
-
|
|
1416
1505
|
const headers = new Headers();
|
|
1417
1506
|
headers.set('cookie', cookieHeader);
|
|
1418
1507
|
|
|
1419
1508
|
const session = await auth.api.getSession({ headers });
|
|
1420
1509
|
if (session?.session?.token) {
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
?.split('/')[0]
|
|
1425
|
-
?.split('?')[0]
|
|
1426
|
-
?.trim()
|
|
1427
|
-
.toLowerCase() || '';
|
|
1428
|
-
|
|
1429
|
-
if (!callbackProvider) {
|
|
1430
|
-
// Do NOT quietly fall back to 'google'. That silent default is the
|
|
1431
|
-
// exact defect this block fixes, and re-introducing it here would
|
|
1432
|
-
// reproduce it for whatever unparseable shape got us here.
|
|
1433
|
-
console.error(
|
|
1434
|
-
'[BETTER_AUTH] OAuth callback matched /callback/ but no provider could be derived from',
|
|
1435
|
-
url.pathname,
|
|
1436
|
-
'— refusing to assert a guessed provider to the IDP. No exchange performed.'
|
|
1437
|
-
);
|
|
1438
|
-
} else {
|
|
1439
|
-
console.log(
|
|
1440
|
-
'[BETTER_AUTH] Got session token from callback:',
|
|
1441
|
-
session.session.token.substring(0, 10),
|
|
1442
|
-
'| provider:', callbackProvider,
|
|
1443
|
-
'| email:', session.user?.email
|
|
1444
|
-
);
|
|
1445
|
-
await exchangeOAuthForIdpTokens(session.session.token, callbackProvider);
|
|
1446
|
-
}
|
|
1510
|
+
sessionToken = session.session.token;
|
|
1511
|
+
console.log('[BETTER_AUTH] Got session token from callback:', tokenLogId(session.session.token), '| email:', session.user?.email);
|
|
1512
|
+
exchanged = await exchangeOAuthForIdpTokens(session.session.token);
|
|
1447
1513
|
} else {
|
|
1448
1514
|
console.warn('[BETTER_AUTH] Could not get session after OAuth callback');
|
|
1449
1515
|
}
|
|
@@ -1451,6 +1517,7 @@ export function createAuthGetHandler(loginPath: string = '/account-auth/login')
|
|
|
1451
1517
|
} catch (err: any) {
|
|
1452
1518
|
console.error('[BETTER_AUTH] IDP token exchange failed:', err.message);
|
|
1453
1519
|
}
|
|
1520
|
+
if (!exchanged) return abortOAuthSignIn(response, sessionToken, cookieHeader, loginPath);
|
|
1454
1521
|
}
|
|
1455
1522
|
|
|
1456
1523
|
return response;
|
|
@@ -215,7 +215,8 @@ export async function idpRefreshToken(
|
|
|
215
215
|
|
|
216
216
|
if (!response.ok) {
|
|
217
217
|
const errorText = await response.text().catch(() => 'Unknown error');
|
|
218
|
-
|
|
218
|
+
// PAY-1427: never the raw upstream body (the refresh.ts:528 twin PR 139 fixed). Status + length only.
|
|
219
|
+
console.error('[IDP_CLIENT] Token refresh failed:', { status: response.status, bodyLength: errorText.length });
|
|
219
220
|
return {
|
|
220
221
|
success: false,
|
|
221
222
|
error: {
|
|
@@ -214,7 +214,10 @@ export function DataBrowserTab({
|
|
|
214
214
|
|
|
215
215
|
if (!response.ok) {
|
|
216
216
|
const errData = await response.json().catch(() => ({}));
|
|
217
|
-
|
|
217
|
+
// PAY-1425: the handler returns a fixed message plus a correlationId (never upstream
|
|
218
|
+
// text). Surface the id so an admin can quote it to an operator.
|
|
219
|
+
const ref = errData.error?.correlationId ? ` (ref ${errData.error.correlationId})` : '';
|
|
220
|
+
throw new Error(`${errData.error?.message || 'Delete failed'}${ref}`);
|
|
218
221
|
}
|
|
219
222
|
|
|
220
223
|
setSuccess(`Record ${id} deleted`);
|
|
@@ -45,6 +45,13 @@ export interface SessionsTabProps {
|
|
|
45
45
|
// COMPONENT
|
|
46
46
|
// -----------------------------------------------------------------------------
|
|
47
47
|
|
|
48
|
+
// PAY-1429: admin handlers return a fixed message plus a correlationId (never upstream or
|
|
49
|
+
// exception text). Surface the id so an admin can quote it to an operator.
|
|
50
|
+
function errorWithRef(data: any, fallback: string): string {
|
|
51
|
+
const msg = typeof data?.error === 'string' ? data.error : fallback;
|
|
52
|
+
return data?.correlationId ? `${msg} (ref ${data.correlationId})` : msg;
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
export function SessionsTab({ isDark = true, apiBasePath = '/api/admin/sessions' }: SessionsTabProps) {
|
|
49
56
|
const [sessions, setSessions] = useState<LoginSession[]>([]);
|
|
50
57
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -80,7 +87,7 @@ export function SessionsTab({ isDark = true, apiBasePath = '/api/admin/sessions'
|
|
|
80
87
|
setSessions(data.sessions || []);
|
|
81
88
|
setLastRefresh(new Date());
|
|
82
89
|
} else {
|
|
83
|
-
setError(data
|
|
90
|
+
setError(errorWithRef(data, 'Failed to fetch sessions'));
|
|
84
91
|
}
|
|
85
92
|
} catch (err) {
|
|
86
93
|
setError('Network error. Please try again.');
|
|
@@ -132,7 +139,7 @@ export function SessionsTab({ isDark = true, apiBasePath = '/api/admin/sessions'
|
|
|
132
139
|
await fetchSessions();
|
|
133
140
|
await fetchStats();
|
|
134
141
|
} else {
|
|
135
|
-
alert(data
|
|
142
|
+
alert(errorWithRef(data, 'Failed to revoke session'));
|
|
136
143
|
}
|
|
137
144
|
} catch (err) {
|
|
138
145
|
alert('Network error. Please try again.');
|
|
@@ -162,7 +169,7 @@ export function SessionsTab({ isDark = true, apiBasePath = '/api/admin/sessions'
|
|
|
162
169
|
await fetchSessions();
|
|
163
170
|
await fetchStats();
|
|
164
171
|
} else {
|
|
165
|
-
alert(data
|
|
172
|
+
alert(errorWithRef(data, 'Failed to revoke sessions'));
|
|
166
173
|
}
|
|
167
174
|
} catch (err) {
|
|
168
175
|
alert('Network error. Please try again.');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
onGoToLogin: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* PAY-1588 (rigpert 60711): what a caller sees when the recovery request produced NO recovery session - an unknown
|
|
11
|
+
* address, a locked account, a throttled caller. ONE sentence, identical for all of them, so the screen cannot be used
|
|
12
|
+
* to tell which. It must not claim a reset happened (that is CompleteStep, reached only after a successful reset) and it
|
|
13
|
+
* must not name a cause.
|
|
14
|
+
*/
|
|
15
|
+
export function RequestedStep({ onGoToLogin }: Props) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="bg-white border border-gray-300 rounded p-8 text-center">
|
|
18
|
+
<h2 className="text-2xl font-semibold text-gray-900 mb-2">Check your email</h2>
|
|
19
|
+
|
|
20
|
+
<p className="text-gray-600 mb-6">If an account exists for that email, we've sent instructions to reset your password.</p>
|
|
21
|
+
|
|
22
|
+
<button
|
|
23
|
+
onClick={onGoToLogin}
|
|
24
|
+
className="w-full py-2 px-4 border border-gray-300 rounded text-gray-900 font-medium hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400"
|
|
25
|
+
>
|
|
26
|
+
Back to sign in
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState } from 'react';
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { usePasswordValidation } from '../../hooks/usePasswordValidation';
|
|
5
|
+
import { passwordRulesText } from '../../lib/password-rules';
|
|
4
6
|
|
|
5
7
|
interface Props {
|
|
6
8
|
onSubmit: (password: string, confirmPassword: string) => void;
|
|
@@ -15,6 +17,14 @@ export function SetPasswordStep({ onSubmit, loading }: Props) {
|
|
|
15
17
|
|
|
16
18
|
const passwordsMatch = password === confirmPassword && password !== '';
|
|
17
19
|
|
|
20
|
+
// PAY-1591: ADVISORY only. It shows the rules and what is missing; it never disables submit, because the validator
|
|
21
|
+
// may be down and the IdP is the authority (a dead validator must not trap a user who already spent their code).
|
|
22
|
+
const { setPassword: validateAsYouType, policy, failedRequirements } = usePasswordValidation();
|
|
23
|
+
// The policy only comes back with a validation, so ask once on mount with a one-character probe (it never leaves the
|
|
24
|
+
// policy question; the answer is what the rules line is built from). Until it lands the line says the rules are coming.
|
|
25
|
+
useEffect(() => { validateAsYouType('a'); }, [validateAsYouType]);
|
|
26
|
+
const missing = failedRequirements.filter((r) => !/^(Unable to validate|Password validation)/i.test(r));
|
|
27
|
+
|
|
18
28
|
const handleSubmit = (e: React.FormEvent) => {
|
|
19
29
|
e.preventDefault();
|
|
20
30
|
if (passwordsMatch) {
|
|
@@ -25,9 +35,12 @@ export function SetPasswordStep({ onSubmit, loading }: Props) {
|
|
|
25
35
|
return (
|
|
26
36
|
<div className="bg-white border border-gray-300 rounded p-8">
|
|
27
37
|
<h2 className="text-xl font-semibold text-gray-900 mb-2">Set New Password</h2>
|
|
28
|
-
<p className="text-sm text-gray-600 mb-
|
|
38
|
+
<p className="text-sm text-gray-600 mb-2">
|
|
29
39
|
Choose a strong password for your account.
|
|
30
40
|
</p>
|
|
41
|
+
<p data-testid="password-rules" className="text-sm text-gray-700 mb-6">
|
|
42
|
+
{passwordRulesText(policy)}
|
|
43
|
+
</p>
|
|
31
44
|
|
|
32
45
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
33
46
|
<div>
|
|
@@ -39,7 +52,7 @@ export function SetPasswordStep({ onSubmit, loading }: Props) {
|
|
|
39
52
|
type={showPassword ? 'text' : 'password'}
|
|
40
53
|
id="password"
|
|
41
54
|
value={password}
|
|
42
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
55
|
+
onChange={(e) => { setPassword(e.target.value); validateAsYouType(e.target.value); }}
|
|
43
56
|
required
|
|
44
57
|
disabled={loading}
|
|
45
58
|
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-gray-900 focus:outline-none disabled:opacity-50 disabled:bg-gray-50 pr-10"
|
|
@@ -78,6 +91,14 @@ export function SetPasswordStep({ onSubmit, loading }: Props) {
|
|
|
78
91
|
</div>
|
|
79
92
|
</div>
|
|
80
93
|
|
|
94
|
+
{password && missing.length > 0 && (
|
|
95
|
+
<ul data-testid="password-missing" className="text-sm text-gray-700 list-disc pl-5">
|
|
96
|
+
{missing.map((r) => (
|
|
97
|
+
<li key={r}>{r}</li>
|
|
98
|
+
))}
|
|
99
|
+
</ul>
|
|
100
|
+
)}
|
|
101
|
+
|
|
81
102
|
{!passwordsMatch && confirmPassword && (
|
|
82
103
|
<div className="bg-red-50 border border-red-200 rounded p-3">
|
|
83
104
|
<p className="text-red-700 text-sm">Passwords do not match</p>
|
package/src/config/env.ts
CHANGED
|
@@ -29,8 +29,6 @@ export const API_ENDPOINTS = {
|
|
|
29
29
|
profile: '/api/account/profile',
|
|
30
30
|
updateProfile: '/api/account/profile',
|
|
31
31
|
maskedInfo: '/api/account/masked-info',
|
|
32
|
-
sendResetCode: '/api/account/send-reset-code',
|
|
33
|
-
verifyResetCode: '/api/account/verify-reset-code',
|
|
34
32
|
resetPassword: '/api/account/reset-password',
|
|
35
33
|
changePassword: '/api/account/change-password',
|
|
36
34
|
validatePassword: '/api/account/validate-password'
|
|
@@ -43,7 +43,8 @@ export interface ViabilityState {
|
|
|
43
43
|
|
|
44
44
|
interface ViabilityResponse {
|
|
45
45
|
authenticated: boolean;
|
|
46
|
-
|
|
46
|
+
/** PAY-1481: tokenLogId of the session token (correlation only); the token itself is never returned. */
|
|
47
|
+
sessionTokenId?: string | null;
|
|
47
48
|
requires2FA?: boolean;
|
|
48
49
|
twoFactorComplete?: boolean;
|
|
49
50
|
accessTokenExpired?: boolean;
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,10 @@ export type { AuthConfig } from './types/auth';
|
|
|
23
23
|
// Route configuration (client-safe)
|
|
24
24
|
export { makeAuthDecision } from './auth/auth-decision';
|
|
25
25
|
export { isUnauthenticatedRoute, configurePublicRoutes, getRouteConfig } from './auth/route-config';
|
|
26
|
+
|
|
27
|
+
// The ONE callback-target guard (N7 / PAY-1989): apps import this instead of
|
|
28
|
+
// copying it. Exposed from the main index AND as the subpath ./auth/safe-callback.
|
|
29
|
+
export { safeCallbackPath, isSafeCallbackPath, resolveCallbackDestination, DEFAULT_CALLBACK } from './auth/safe-callback';
|
|
26
30
|
export { createMvpMiddleware } from './middleware/create-middleware';
|
|
27
31
|
|
|
28
32
|
// Account Components
|
package/src/lib/app-slug.ts
CHANGED
|
@@ -27,6 +27,56 @@ export function getAppSlug(): string {
|
|
|
27
27
|
return cachedSlug;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The session cookie name has TWO producers: better-auth mints `${idpConfig.clientSlug || getAppSlug()}.session-token`, while
|
|
32
|
+
* the gate and every reader use getSessionCookieName() = getAppSlug() = APP_SLUG || CLIENT_ID. When the IdP's slug is known and
|
|
33
|
+
* differs, the gate looks for a cookie that was never set and every better-auth (Google) session bounces to
|
|
34
|
+
* /sign-in?error=SessionInvalid straight after a successful 2FA (rigpert 60743, live on :4797 with CLIENT_ID=idp-admin-ui-graft
|
|
35
|
+
* against clientSlug idp_admin_ui_graft). FAIL CLOSED: refuse rather than run with two names. No IdP slug -> nothing to compare.
|
|
36
|
+
*/
|
|
37
|
+
export function slugMismatchReason(idpClientSlug: string | null | undefined): string | null {
|
|
38
|
+
if (!idpClientSlug) return null;
|
|
39
|
+
const gateSlug = getAppSlug();
|
|
40
|
+
return idpClientSlug !== gateSlug ? `SLUG_MISMATCH: cookie=${idpClientSlug} gate=${gateSlug}` : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* rigpert 60813: when the IdP returns NO client slug for this app (an unknown client: the config fetch failed or came back
|
|
45
|
+
* without one), startup must fail LOUD. Silently defaulting would run with a cookie name nobody verified against the IdP. The
|
|
46
|
+
* startup path swallows a failed config fetch when BETTER_AUTH_SECRET is supplied by env, so without this an unregistered
|
|
47
|
+
* CLIENT_ID (e.g. payez_idp_customer_web, absent on dev-93) would boot 'fine'.
|
|
48
|
+
*/
|
|
49
|
+
export function assertIdpSlugKnown(idpClientSlug: string | null | undefined): void {
|
|
50
|
+
if (idpClientSlug) return;
|
|
51
|
+
throw new Error(
|
|
52
|
+
`[FATAL] The IdP returned no client slug for this app (gate slug "${getAppSlug()}"). Either CLIENT_ID/APP_SLUG names a client ` +
|
|
53
|
+
`the IdP does not know, or its config could not be fetched. Refusing to start with an unverified session cookie name.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The health-endpoint view: a mismatch is always reportable; an UNKNOWN slug is reported only once startup has actually been
|
|
59
|
+
* attempted (before that nothing is known yet and health must not go red on a pod that has not started initialising).
|
|
60
|
+
*/
|
|
61
|
+
export function slugStateReason(idpClientSlug: string | null | undefined, startupAttempted: boolean): string | null {
|
|
62
|
+
const mismatch = slugMismatchReason(idpClientSlug);
|
|
63
|
+
if (mismatch) return mismatch;
|
|
64
|
+
if (startupAttempted && !idpClientSlug) return `SLUG_UNKNOWN: the IdP returned no client slug for this app gate=${getAppSlug()}`;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function assertAppSlugMatchesIdp(idpClientSlug: string | null | undefined): void {
|
|
69
|
+
if (!idpClientSlug) return;
|
|
70
|
+
const gateSlug = getAppSlug();
|
|
71
|
+
if (slugMismatchReason(idpClientSlug) !== null) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`[FATAL] Session cookie name would have two sources: the IdP client slug is "${idpClientSlug}" but this app's slug ` +
|
|
74
|
+
`(APP_SLUG, else CLIENT_ID) is "${gateSlug}". better-auth would set "${idpClientSlug}.session-token" while the gate reads ` +
|
|
75
|
+
`"${gateSlug}.session-token", so every better-auth session would be rejected. Set APP_SLUG (or CLIENT_ID) to "${idpClientSlug}".`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
30
80
|
/**
|
|
31
81
|
* Clears the cached slug (useful for testing).
|
|
32
82
|
*/
|