@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
|
@@ -33,11 +33,16 @@
|
|
|
33
33
|
* instead of degrading to one clean Redis-canonical lookup (or a genuine
|
|
34
34
|
* 401 if the record really is gone).
|
|
35
35
|
*
|
|
36
|
-
* No signature is verified
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
36
|
+
* No signature is verified here, and none CAN be: this module is secret-free
|
|
37
|
+
* and synchronous. It only CLASSIFIES and recovers the candidate token.
|
|
38
|
+
*
|
|
39
|
+
* PAY-1476: the earlier claim that "Redis is the liveness authority, so a
|
|
40
|
+
* forged token still has to land on a live session record" was the defect.
|
|
41
|
+
* A LEAKED token lands on a live record by definition, so without a signature
|
|
42
|
+
* check the token string alone was a bearer credential. The candidate token
|
|
43
|
+
* returned here must NEVER be handed to the session store directly: go through
|
|
44
|
+
* server/verified-session-token.ts resolveVerifiedSessionToken, which verifies
|
|
45
|
+
* the HMAC (ba-hmac) or the JWS signature first.
|
|
41
46
|
*/
|
|
42
47
|
export type SessionCookieShape = 'jws' | 'ba-hmac';
|
|
43
48
|
export interface ExtractedSessionToken {
|
|
@@ -53,17 +58,15 @@ export interface ExtractedSessionToken {
|
|
|
53
58
|
* claim.
|
|
54
59
|
*/
|
|
55
60
|
export declare function extractSessionTokenFromCookieValue(rawValue: string | null | undefined): ExtractedSessionToken | null;
|
|
56
|
-
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
|
|
68
|
-
* present or neither known shape matches. */
|
|
69
|
-
export declare function extractSessionTokenFromCookie(request: Request): string | null;
|
|
61
|
+
/** The RAW session-cookie value from a Request, WITHOUT going through Better
|
|
62
|
+
* Auth's `auth.api.getSession()`. Handles single and chunked cookies (Better
|
|
63
|
+
* Auth chunks long values across `{name}.0`, `{name}.1`, …) and every name the
|
|
64
|
+
* cookie is minted under (240654).
|
|
65
|
+
*
|
|
66
|
+
* PAY-1476: this used to be `extractSessionTokenFromCookie`, which returned the
|
|
67
|
+
* TOKEN with "No signature check — Redis is the liveness gate". That turned any
|
|
68
|
+
* leaked token into a bearer credential. It now returns the raw value only, so
|
|
69
|
+
* the one way to get a token out of a request cookie is
|
|
70
|
+
* server/verified-session-token.ts resolveVerifiedSessionTokenFromRequest.
|
|
71
|
+
* Returns null if no session cookie is present. */
|
|
72
|
+
export declare function extractSessionCookieValue(request: Request): string | null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.extractSessionTokenFromCookieValue = extractSessionTokenFromCookieValue;
|
|
4
|
-
exports.
|
|
4
|
+
exports.extractSessionCookieValue = extractSessionCookieValue;
|
|
5
5
|
const app_slug_1 = require("./app-slug");
|
|
6
6
|
/**
|
|
7
7
|
* Extract a candidate session token from a raw session-cookie value,
|
|
@@ -37,20 +37,18 @@ function extractSessionTokenFromCookieValue(rawValue) {
|
|
|
37
37
|
}
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
* present or neither known shape matches. */
|
|
53
|
-
function extractSessionTokenFromCookie(request) {
|
|
40
|
+
/** The RAW session-cookie value from a Request, WITHOUT going through Better
|
|
41
|
+
* Auth's `auth.api.getSession()`. Handles single and chunked cookies (Better
|
|
42
|
+
* Auth chunks long values across `{name}.0`, `{name}.1`, …) and every name the
|
|
43
|
+
* cookie is minted under (240654).
|
|
44
|
+
*
|
|
45
|
+
* PAY-1476: this used to be `extractSessionTokenFromCookie`, which returned the
|
|
46
|
+
* TOKEN with "No signature check — Redis is the liveness gate". That turned any
|
|
47
|
+
* leaked token into a bearer credential. It now returns the raw value only, so
|
|
48
|
+
* the one way to get a token out of a request cookie is
|
|
49
|
+
* server/verified-session-token.ts resolveVerifiedSessionTokenFromRequest.
|
|
50
|
+
* Returns null if no session cookie is present. */
|
|
51
|
+
function extractSessionCookieValue(request) {
|
|
54
52
|
const cookieHeader = request.headers.get('cookie');
|
|
55
53
|
if (!cookieHeader)
|
|
56
54
|
return null;
|
|
@@ -120,7 +118,5 @@ function extractSessionTokenFromCookie(request) {
|
|
|
120
118
|
break;
|
|
121
119
|
}
|
|
122
120
|
}
|
|
123
|
-
|
|
124
|
-
return null;
|
|
125
|
-
return extractSessionTokenFromCookieValue(rawJwt)?.token ?? null;
|
|
121
|
+
return rawJwt || null;
|
|
126
122
|
}
|
|
@@ -101,9 +101,16 @@ export declare function deleteSessionEverywhere(sessionToken: string, appSlug?:
|
|
|
101
101
|
export declare function setSession(sessionToken: string, data: SessionData): Promise<void>;
|
|
102
102
|
/**
|
|
103
103
|
* Updates tokens within an existing session.
|
|
104
|
+
*
|
|
105
|
+
* CALLERS MUST CHECK THE RETURN VALUE (320072). A null return means the session
|
|
106
|
+
* row was absent and EVERY field in `updates` was discarded — it is a dropped
|
|
107
|
+
* write, not a no-op on unchanged data. Ignoring it is how a token refresh came
|
|
108
|
+
* to report success while persisting nothing.
|
|
109
|
+
*
|
|
104
110
|
* @param sessionToken The session token to update.
|
|
105
111
|
* @param updates Partial session data to update.
|
|
106
|
-
* @returns The updated session data, or null if the session was not found
|
|
112
|
+
* @returns The updated session data, or null if the session was not found — in
|
|
113
|
+
* which case NOTHING WAS WRITTEN.
|
|
107
114
|
*/
|
|
108
115
|
export declare function updateSession(sessionToken: string, updates: Partial<SessionData>): Promise<SessionData | null>;
|
|
109
116
|
/**
|
|
@@ -41,6 +41,7 @@ const redis_1 = __importDefault(require("./redis"));
|
|
|
41
41
|
const crypto_1 = require("crypto");
|
|
42
42
|
const app_slug_1 = require("./app-slug");
|
|
43
43
|
const token_utils_1 = require("../auth/utils/token-utils");
|
|
44
|
+
const token_log_id_1 = require("./token-log-id");
|
|
44
45
|
// Use app-slug prefixes for multi-app isolation
|
|
45
46
|
const getSessionKey = (token) => `${(0, app_slug_1.getSessionPrefix)()}${token}`;
|
|
46
47
|
const getRefreshLockKey = (token) => `${(0, app_slug_1.getRefreshLockPrefix)()}${token}`;
|
|
@@ -278,11 +279,31 @@ async function setSession(sessionToken, data) {
|
|
|
278
279
|
.expire(versionKey, exports.SESSION_TTL)
|
|
279
280
|
.exec();
|
|
280
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Describe a credential-bearing value for a log line WITHOUT the value: its kind only.
|
|
284
|
+
* Deliberately no length or fingerprint either - both narrow the value down.
|
|
285
|
+
*/
|
|
286
|
+
function describeTokenValueForLog(v) {
|
|
287
|
+
if (v === undefined)
|
|
288
|
+
return 'undefined';
|
|
289
|
+
if (v === null)
|
|
290
|
+
return 'null';
|
|
291
|
+
if (typeof v === 'string')
|
|
292
|
+
return v.length === 0 ? 'empty string' : 'non-empty string (value withheld)';
|
|
293
|
+
return `${typeof v} (value withheld)`;
|
|
294
|
+
}
|
|
281
295
|
/**
|
|
282
296
|
* Updates tokens within an existing session.
|
|
297
|
+
*
|
|
298
|
+
* CALLERS MUST CHECK THE RETURN VALUE (320072). A null return means the session
|
|
299
|
+
* row was absent and EVERY field in `updates` was discarded — it is a dropped
|
|
300
|
+
* write, not a no-op on unchanged data. Ignoring it is how a token refresh came
|
|
301
|
+
* to report success while persisting nothing.
|
|
302
|
+
*
|
|
283
303
|
* @param sessionToken The session token to update.
|
|
284
304
|
* @param updates Partial session data to update.
|
|
285
|
-
* @returns The updated session data, or null if the session was not found
|
|
305
|
+
* @returns The updated session data, or null if the session was not found — in
|
|
306
|
+
* which case NOTHING WAS WRITTEN.
|
|
286
307
|
*/
|
|
287
308
|
async function updateSession(sessionToken, updates) {
|
|
288
309
|
const key = getSessionKey(sessionToken);
|
|
@@ -290,6 +311,74 @@ async function updateSession(sessionToken, updates) {
|
|
|
290
311
|
// Get current session to merge with updates
|
|
291
312
|
const currentSession = await getSession(sessionToken);
|
|
292
313
|
if (!currentSession) {
|
|
314
|
+
/**
|
|
315
|
+
* 320072: THIS IS A DROPPED WRITE, AND IT USED TO LEAVE NO TRACE.
|
|
316
|
+
*
|
|
317
|
+
* Returning null here is correct — there is no row to merge into — but it
|
|
318
|
+
* was previously done in total silence: no throw, no log. Every caller that
|
|
319
|
+
* ignores the return value therefore believed it had persisted, and the
|
|
320
|
+
* most damaging caller did exactly that (ensure-fresh-access-token).
|
|
321
|
+
*
|
|
322
|
+
* The canonical row can legitimately be absent: better-auth.ts writes the
|
|
323
|
+
* `ba:` record and the canonical row in two steps, and the second is
|
|
324
|
+
* wrapped in a try/catch that only console.warn()s — a deliberate,
|
|
325
|
+
* documented degradation that leaves the BA session correct and this row
|
|
326
|
+
* missing. That decision is fine. What was NOT fine is that the READ side
|
|
327
|
+
* reported success anyway, so nothing ever surfaced it.
|
|
328
|
+
*
|
|
329
|
+
* Logged at error, not warn: the caller's data is gone. Callers that can
|
|
330
|
+
* tolerate a missing row should check the null return — which is now the
|
|
331
|
+
* documented contract — rather than rely on this being quiet.
|
|
332
|
+
*/
|
|
333
|
+
/**
|
|
334
|
+
* DO NOT LOG `key` HERE. `getSessionKey()` (line 21) is
|
|
335
|
+
* `${getSessionPrefix()}${token}` — the prefix plus the WHOLE session
|
|
336
|
+
* token — so logging it writes a live, replayable credential into
|
|
337
|
+
* console.error, and logs travel further than the process that wrote them.
|
|
338
|
+
* The redacting idiom in this file is `substring(0, 8) + '...'`.
|
|
339
|
+
*
|
|
340
|
+
* NOTE ON WHAT THIS COMMENT NO LONGER CLAIMS. It used to say this was the
|
|
341
|
+
* single unredacted departure in the file, and gave a site count. Both were
|
|
342
|
+
* wrong: a RAW refresh token was being emitted at the
|
|
343
|
+
* REFRESH_TOKEN_BEING_CLEARED site below, and the count was stale. QAPert found that
|
|
344
|
+
* leak while reviewing PR 96 — he looked *because* this comment asserted
|
|
345
|
+
* there was nothing to find, which is the worst thing a comment can do.
|
|
346
|
+
* It is fixed in the same commit as this rewrite.
|
|
347
|
+
*
|
|
348
|
+
* So the counts are gone deliberately. A tally in prose decays silently on
|
|
349
|
+
* the next edit and then actively misleads; that decay is the defect being
|
|
350
|
+
* repaired here, and reproducing it in the fix would be absurd. If you need
|
|
351
|
+
* a number, measure it — do not trust this paragraph for one.
|
|
352
|
+
*
|
|
353
|
+
* What replaces the prose is enforcement. The invariant is
|
|
354
|
+
* NO RAW CREDENTIAL IS EVER EMITTED FROM THIS FUNCTION, and it is pinned by
|
|
355
|
+
* executable assertions, not by this text:
|
|
356
|
+
* - Case 4 (session token, this log)
|
|
357
|
+
* - Case 5 (refresh token, the REFRESH_TOKEN_BEING_CLEARED log below)
|
|
358
|
+
* in test/ensure-fresh-persist-failure.test.cjs. Both were proven to fail
|
|
359
|
+
* when their fix is mutated out. Delete a redaction and a test goes red;
|
|
360
|
+
* delete this comment and nothing does. Trust the tests.
|
|
361
|
+
*
|
|
362
|
+
* Caught by QAPert on 320072 and made binding by BAPert (R1043 s1).
|
|
363
|
+
*
|
|
364
|
+
* The prefix is logged SEPARATELY and deliberately, because it is the field
|
|
365
|
+
* that makes this line diagnostic rather than decorative: a "no canonical
|
|
366
|
+
* row" miss has two very different causes, and only the prefix tells them
|
|
367
|
+
* apart —
|
|
368
|
+
* wrong keyspace (slug/prefix disagreement — the 241875 / 213416 defect
|
|
369
|
+
* documented at line 111, where a writer and a reader
|
|
370
|
+
* resolved different slugs and every lookup silently
|
|
371
|
+
* missed), versus
|
|
372
|
+
* absent row (the documented better-auth.ts two-step degradation).
|
|
373
|
+
* The prefix is an app slug, not a secret. Redacting it too would leave a
|
|
374
|
+
* log that says only "something was missing".
|
|
375
|
+
*/
|
|
376
|
+
console.error('[SESSION-STORE] UPDATE_DROPPED_NO_CANONICAL_ROW', {
|
|
377
|
+
keyPrefix: (0, app_slug_1.getSessionPrefix)(),
|
|
378
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
379
|
+
updatedFields: Object.keys(updates),
|
|
380
|
+
note: 'updateSession found no session row; ALL updates were discarded and null was returned',
|
|
381
|
+
});
|
|
293
382
|
return null;
|
|
294
383
|
}
|
|
295
384
|
// CRITICAL: Track any refresh token changes (check both old and new field names)
|
|
@@ -301,10 +390,15 @@ async function updateSession(sessionToken, updates) {
|
|
|
301
390
|
: hadRefreshToken;
|
|
302
391
|
if (hadRefreshToken && !willHaveRefreshToken) {
|
|
303
392
|
console.error('[SESSION-STORE] ⚠️ REFRESH_TOKEN_BEING_CLEARED', {
|
|
304
|
-
sessionToken:
|
|
393
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
305
394
|
userId: currentSession.userId,
|
|
306
395
|
updateKeys: Object.keys(updates),
|
|
307
|
-
|
|
396
|
+
// NEVER the value: a refresh token is a long-lived credential and this line ships to the
|
|
397
|
+
// log sink. The old `idpRefreshToken || refreshToken` printed a live legacy token raw when
|
|
398
|
+
// idpRefreshToken was null/"" (which clears) and refreshToken still held one.
|
|
399
|
+
// Test: test/session-store-refresh-token-not-logged.test.cjs
|
|
400
|
+
idpRefreshTokenInUpdate: describeTokenValueForLog(updates.idpRefreshToken),
|
|
401
|
+
legacyRefreshTokenInUpdate: describeTokenValueForLog(updates.refreshToken),
|
|
308
402
|
refreshTokenClearedReason: updates.refreshTokenClearedReason || 'UNKNOWN',
|
|
309
403
|
stack: new Error().stack
|
|
310
404
|
});
|
|
@@ -354,15 +448,67 @@ function mfaLifetimeFromAccessToken(accessToken) {
|
|
|
354
448
|
if (!decoded)
|
|
355
449
|
return {};
|
|
356
450
|
const fields = {};
|
|
451
|
+
/**
|
|
452
|
+
* *** TEST THE CONVERTED VALUE, NOT THE CLAIM'S PRESENCE. ***
|
|
453
|
+
*
|
|
454
|
+
* This guard previously branched on `claim !== undefined && claim !== null`
|
|
455
|
+
* and then wrote `Number(claim) * 1000`. Those are two different questions,
|
|
456
|
+
* and for a non-numeric claim they disagree: the claim IS present, so the
|
|
457
|
+
* branch is taken, and the value written is NaN. That single NaN defeats both
|
|
458
|
+
* guards in this system at once, in opposite directions:
|
|
459
|
+
*
|
|
460
|
+
* - `mfaEvidencePresent = mfaLifetime.mfaExpiresAt !== undefined` is TRUE
|
|
461
|
+
* for NaN, so R908(e) certifies a ceremony it never observed and GRANTS
|
|
462
|
+
* mfaVerified.
|
|
463
|
+
* - consumers read `mfaExpiresAt || 0`, and NaN is FALSY, so that collapses
|
|
464
|
+
* to 0, `0 > 0` is false, and the session NEVER expires.
|
|
465
|
+
*
|
|
466
|
+
* Net: `mfaVerified: true` that nothing can ever expire — the exact
|
|
467
|
+
* pre-339563 fail-open described at the top of this function, restored
|
|
468
|
+
* through a malformed claim, and silent (neither warning below fires on NaN).
|
|
469
|
+
*
|
|
470
|
+
* So a claim that will not convert is treated as NO CLAIM: the key is OMITTED
|
|
471
|
+
* and R908(e) withholds the grant, identical to the absent-claim lane. Absent
|
|
472
|
+
* and unreadable are the same epistemic state — we do not know the expiry —
|
|
473
|
+
* and they must not produce opposite security outcomes.
|
|
474
|
+
*
|
|
475
|
+
* Omitted, never set-to-undefined: updateSession() merges with
|
|
476
|
+
* `{ ...current, ...updates }`, so a present-but-undefined key would CLOBBER
|
|
477
|
+
* a good stored value. Pinned by test/mfa-expiry-malformed-claim.test.cjs.
|
|
478
|
+
*
|
|
479
|
+
* *** `Number.isFinite` ALONE IS NOT ENOUGH — IT MUST ALSO BE POSITIVE. ***
|
|
480
|
+
* `Number([])` is 0, and 0 is perfectly finite. But the consumer guard reads
|
|
481
|
+
* `mfaExpiresAt || 0`, and 0 is FALSY, so a stored zero is indistinguishable
|
|
482
|
+
* from a missing value and fails open in precisely the same way NaN does —
|
|
483
|
+
* while still passing `!== undefined` and so still earning the R908(e) grant.
|
|
484
|
+
* The honest invariant is not "is it a number" but "can this value ever
|
|
485
|
+
* expire a session", and only a positive one can. A zero/negative
|
|
486
|
+
* `mfa_expires` is meaningless anyway (epoch 1970 or earlier).
|
|
487
|
+
*
|
|
488
|
+
* The `typeof` gate rejects objects and arrays outright rather than letting
|
|
489
|
+
* JS coercion invent a number for them: `[]` -> 0 and `[5]` -> 5 are accidents
|
|
490
|
+
* of coercion, not claims the IDP made.
|
|
491
|
+
*/
|
|
492
|
+
const claimToNumber = (claim) => {
|
|
493
|
+
if (typeof claim !== 'number' && typeof claim !== 'string')
|
|
494
|
+
return undefined;
|
|
495
|
+
if (claim === '')
|
|
496
|
+
return undefined;
|
|
497
|
+
const n = Number(claim);
|
|
498
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
499
|
+
};
|
|
357
500
|
// Claims are Unix SECONDS; SessionData stores milliseconds.
|
|
358
|
-
|
|
359
|
-
|
|
501
|
+
const mfaExpires = claimToNumber(decoded.mfa_expires);
|
|
502
|
+
if (mfaExpires !== undefined) {
|
|
503
|
+
fields.mfaExpiresAt = mfaExpires * 1000;
|
|
360
504
|
}
|
|
361
|
-
|
|
362
|
-
|
|
505
|
+
const mfaTime = claimToNumber(decoded.mfa_time);
|
|
506
|
+
if (mfaTime !== undefined) {
|
|
507
|
+
fields.mfaCompletedAt = mfaTime * 1000;
|
|
363
508
|
}
|
|
364
|
-
|
|
365
|
-
|
|
509
|
+
const mfaValidityHours = claimToNumber(decoded.mfa_validity_hours);
|
|
510
|
+
if (mfaValidityHours !== undefined) {
|
|
511
|
+
fields.mfaValidityHours = mfaValidityHours;
|
|
366
512
|
}
|
|
367
513
|
return fields;
|
|
368
514
|
}
|
|
@@ -376,7 +522,7 @@ function mfaLifetimeFromAccessToken(accessToken) {
|
|
|
376
522
|
async function transitionTo2FASession(sessionToken, tokens, mfaMethod) {
|
|
377
523
|
const newAccessToken = tokens.idpAccessToken || tokens.accessToken;
|
|
378
524
|
console.log('[transitionTo2FASession] Called with:', {
|
|
379
|
-
sessionToken:
|
|
525
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
380
526
|
mfaMethod,
|
|
381
527
|
hasAccessToken: !!newAccessToken,
|
|
382
528
|
hasRefreshToken: !!(tokens.idpRefreshToken || tokens.refreshToken),
|
|
@@ -433,7 +579,7 @@ async function transitionTo2FASession(sessionToken, tokens, mfaMethod) {
|
|
|
433
579
|
'mfaVerified; setting nextStep=mfa_verify so the consumer re-challenges. ' +
|
|
434
580
|
'If this fires for a lane that DID complete a real ceremony, the defect is ' +
|
|
435
581
|
'on the emit side (R908(b)) - do not "fix" it by minting the flag here.', {
|
|
436
|
-
sessionToken:
|
|
582
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
437
583
|
mfaMethod,
|
|
438
584
|
hasAccessToken: !!newAccessToken,
|
|
439
585
|
});
|
|
@@ -603,7 +749,7 @@ async function acquireRefreshLock(sessionToken, requestId, maxWaitMs = 5000) {
|
|
|
603
749
|
const result = await redis_1.default.set(lockKey, JSON.stringify(lockInfo), 'PX', REFRESH_LOCK_TTL * 1000, 'NX');
|
|
604
750
|
if (result === 'OK') {
|
|
605
751
|
console.debug('[SESSION-STORE] Refresh lock acquired', {
|
|
606
|
-
sessionToken:
|
|
752
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
607
753
|
requestId,
|
|
608
754
|
lockVersion
|
|
609
755
|
});
|
|
@@ -613,14 +759,14 @@ async function acquireRefreshLock(sessionToken, requestId, maxWaitMs = 5000) {
|
|
|
613
759
|
// Lock already exists, check if we should wait
|
|
614
760
|
if (maxWaitMs > 0) {
|
|
615
761
|
console.debug('[SESSION-STORE] Refresh lock already exists, waiting for release', {
|
|
616
|
-
sessionToken:
|
|
762
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
617
763
|
requestId,
|
|
618
764
|
maxWaitMs
|
|
619
765
|
});
|
|
620
766
|
return await waitForRefreshLockRelease(sessionToken, requestId, maxWaitMs);
|
|
621
767
|
}
|
|
622
768
|
console.warn('[SESSION-STORE] Refresh lock already exists, not waiting', {
|
|
623
|
-
sessionToken:
|
|
769
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
624
770
|
requestId
|
|
625
771
|
});
|
|
626
772
|
return { acquired: false };
|
|
@@ -628,7 +774,7 @@ async function acquireRefreshLock(sessionToken, requestId, maxWaitMs = 5000) {
|
|
|
628
774
|
}
|
|
629
775
|
catch (error) {
|
|
630
776
|
console.error('[SESSION-STORE] Failed to acquire refresh lock', {
|
|
631
|
-
sessionToken:
|
|
777
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
632
778
|
requestId,
|
|
633
779
|
error: error instanceof Error ? error.message : String(error)
|
|
634
780
|
});
|
|
@@ -654,7 +800,7 @@ async function waitForRefreshLockRelease(sessionToken, requestId, maxWaitMs) {
|
|
|
654
800
|
}
|
|
655
801
|
catch (error) {
|
|
656
802
|
console.error('[SESSION-STORE] Error while waiting for refresh lock release', {
|
|
657
|
-
sessionToken:
|
|
803
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
658
804
|
requestId,
|
|
659
805
|
error: error instanceof Error ? error.message : String(error)
|
|
660
806
|
});
|
|
@@ -662,7 +808,7 @@ async function waitForRefreshLockRelease(sessionToken, requestId, maxWaitMs) {
|
|
|
662
808
|
}
|
|
663
809
|
}
|
|
664
810
|
console.warn('[SESSION-STORE] Timeout waiting for refresh lock release', {
|
|
665
|
-
sessionToken:
|
|
811
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
666
812
|
requestId,
|
|
667
813
|
waitedMs: Date.now() - startTime
|
|
668
814
|
});
|
|
@@ -701,7 +847,7 @@ async function releaseRefreshLock(sessionToken, requestId, lockVersion) {
|
|
|
701
847
|
const result = await redis_1.default.eval(luaScript, 1, lockKey, requestId, lockVersion ? lockVersion.toString() : '');
|
|
702
848
|
if (result === 1) {
|
|
703
849
|
console.debug('[SESSION-STORE] Refresh lock released successfully', {
|
|
704
|
-
sessionToken:
|
|
850
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
705
851
|
requestId,
|
|
706
852
|
lockVersion
|
|
707
853
|
});
|
|
@@ -709,21 +855,21 @@ async function releaseRefreshLock(sessionToken, requestId, lockVersion) {
|
|
|
709
855
|
}
|
|
710
856
|
else if (result === 0) {
|
|
711
857
|
console.warn('[SESSION-STORE] Attempted to release non-existent refresh lock', {
|
|
712
|
-
sessionToken:
|
|
858
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
713
859
|
requestId
|
|
714
860
|
});
|
|
715
861
|
return false;
|
|
716
862
|
}
|
|
717
863
|
else if (result === -1) {
|
|
718
864
|
console.error('[SESSION-STORE] Attempted to release refresh lock owned by another request', {
|
|
719
|
-
sessionToken:
|
|
865
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
720
866
|
requestId
|
|
721
867
|
});
|
|
722
868
|
return false;
|
|
723
869
|
}
|
|
724
870
|
else if (result === -2) {
|
|
725
871
|
console.error('[SESSION-STORE] Lock version mismatch during release', {
|
|
726
|
-
sessionToken:
|
|
872
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
727
873
|
requestId,
|
|
728
874
|
lockVersion
|
|
729
875
|
});
|
|
@@ -731,7 +877,7 @@ async function releaseRefreshLock(sessionToken, requestId, lockVersion) {
|
|
|
731
877
|
}
|
|
732
878
|
else {
|
|
733
879
|
console.error('[SESSION-STORE] Unexpected result from lock release script', {
|
|
734
|
-
sessionToken:
|
|
880
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
735
881
|
requestId,
|
|
736
882
|
result
|
|
737
883
|
});
|
|
@@ -740,7 +886,7 @@ async function releaseRefreshLock(sessionToken, requestId, lockVersion) {
|
|
|
740
886
|
}
|
|
741
887
|
catch (error) {
|
|
742
888
|
console.error('[SESSION-STORE] Failed to release refresh lock', {
|
|
743
|
-
sessionToken:
|
|
889
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
744
890
|
requestId,
|
|
745
891
|
error: error instanceof Error ? error.message : String(error)
|
|
746
892
|
});
|
|
@@ -761,7 +907,7 @@ async function checkRefreshLock(sessionToken) {
|
|
|
761
907
|
}
|
|
762
908
|
catch (error) {
|
|
763
909
|
console.error('[SESSION-STORE] Failed to check refresh lock', {
|
|
764
|
-
sessionToken:
|
|
910
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
765
911
|
error: error instanceof Error ? error.message : String(error)
|
|
766
912
|
});
|
|
767
913
|
return null;
|
|
@@ -25,6 +25,12 @@ export declare function logStartupStatus(): void;
|
|
|
25
25
|
* Get the cached IDP config after initialization.
|
|
26
26
|
* Returns null if not yet initialized.
|
|
27
27
|
*/
|
|
28
|
+
/**
|
|
29
|
+
* For health endpoints (rigpert 60765): a guard that does not stop the process is an empty red unless the mismatch is visible
|
|
30
|
+
* where operators and promote-fe look. Returns `SLUG_MISMATCH: cookie=<idp slug> gate=<app slug>` while the drift holds, else null;
|
|
31
|
+
* a health route should answer non-200 with this as the reason. Null until the IdP config has been fetched.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getSlugMismatchReason(): string | null;
|
|
28
34
|
export declare function getStartupIDPConfig(): IDPClientConfig | null;
|
|
29
35
|
/**
|
|
30
36
|
* Check if initialization failed (auth signing secret couldn't be retrieved)
|
package/dist/lib/startup-init.js
CHANGED
|
@@ -46,12 +46,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
46
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
47
|
exports.ensureInitialized = ensureInitialized;
|
|
48
48
|
exports.logStartupStatus = logStartupStatus;
|
|
49
|
+
exports.getSlugMismatchReason = getSlugMismatchReason;
|
|
49
50
|
exports.getStartupIDPConfig = getStartupIDPConfig;
|
|
50
51
|
exports.isInitializationFailed = isInitializationFailed;
|
|
51
52
|
exports.getInitializationError = getInitializationError;
|
|
52
53
|
exports.isAppReady = isAppReady;
|
|
53
54
|
require("server-only");
|
|
54
55
|
const idp_client_config_1 = require("./idp-client-config");
|
|
56
|
+
const app_slug_1 = require("./app-slug");
|
|
55
57
|
let initializationStarted = false;
|
|
56
58
|
let initializationComplete = false;
|
|
57
59
|
let initializationFailed = false;
|
|
@@ -90,9 +92,22 @@ async function ensureInitialized() {
|
|
|
90
92
|
lastStartupAttemptTime = now;
|
|
91
93
|
// Mark as started
|
|
92
94
|
initializationStarted = true;
|
|
93
|
-
// Start initialization
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
// Start initialization.
|
|
96
|
+
//
|
|
97
|
+
// PAY-642: a FAILED attempt must not stay installed. This used to be
|
|
98
|
+
// `initializationPromise = performInitialization(); await initializationPromise;` with no reset, and the
|
|
99
|
+
// in-progress check above runs BEFORE the backoff check, so a pod that booted while the IdP was down replayed
|
|
100
|
+
// its first rejection to every caller forever: the 30 s backoff could never lead to a retry. Same latch, same
|
|
101
|
+
// fix, as createSingleFlight (lib/single-flight.ts): callers of the failing attempt still see the rejection
|
|
102
|
+
// (loud), then the attempt clears itself, identity-guarded so a slow failure never evicts a newer attempt.
|
|
103
|
+
// After a failure the backoff check above governs when the next attempt may start.
|
|
104
|
+
const attempt = performInitialization();
|
|
105
|
+
initializationPromise = attempt;
|
|
106
|
+
attempt.catch(() => {
|
|
107
|
+
if (initializationPromise === attempt)
|
|
108
|
+
initializationPromise = null;
|
|
109
|
+
});
|
|
110
|
+
await attempt;
|
|
96
111
|
}
|
|
97
112
|
/**
|
|
98
113
|
* Synchronously log startup status
|
|
@@ -190,8 +205,11 @@ async function performInitialization() {
|
|
|
190
205
|
// Step 3: Validate cookie name consistency
|
|
191
206
|
// This catches bugs where getJwtCookieName() returns a different name than
|
|
192
207
|
// what auth-options.ts configures, which causes sessions to fail in production
|
|
193
|
-
const { validateCookieNameConsistency, getSessionCookieName } = await Promise.resolve().then(() => __importStar(require('./app-slug')));
|
|
208
|
+
const { validateCookieNameConsistency, getSessionCookieName, assertAppSlugMatchesIdp, assertIdpSlugKnown } = await Promise.resolve().then(() => __importStar(require('./app-slug')));
|
|
194
209
|
validateCookieNameConsistency();
|
|
210
|
+
// rigpert 60743: refuse to run with two cookie-name sources (IdP clientSlug vs APP_SLUG/CLIENT_ID).
|
|
211
|
+
assertIdpSlugKnown(cachedIDPConfig?.clientSlug);
|
|
212
|
+
assertAppSlugMatchesIdp(cachedIDPConfig?.clientSlug);
|
|
195
213
|
console.log('[STARTUP] Cookie name consistency validated:', getSessionCookieName());
|
|
196
214
|
// All done
|
|
197
215
|
console.log('');
|
|
@@ -241,6 +259,14 @@ ${connectionLine}║ Possible causes:
|
|
|
241
259
|
* Get the cached IDP config after initialization.
|
|
242
260
|
* Returns null if not yet initialized.
|
|
243
261
|
*/
|
|
262
|
+
/**
|
|
263
|
+
* For health endpoints (rigpert 60765): a guard that does not stop the process is an empty red unless the mismatch is visible
|
|
264
|
+
* where operators and promote-fe look. Returns `SLUG_MISMATCH: cookie=<idp slug> gate=<app slug>` while the drift holds, else null;
|
|
265
|
+
* a health route should answer non-200 with this as the reason. Null until the IdP config has been fetched.
|
|
266
|
+
*/
|
|
267
|
+
function getSlugMismatchReason() {
|
|
268
|
+
return (0, app_slug_1.slugStateReason)(cachedIDPConfig?.clientSlug, initializationComplete || initializationFailed);
|
|
269
|
+
}
|
|
244
270
|
function getStartupIDPConfig() {
|
|
245
271
|
return cachedIDPConfig;
|
|
246
272
|
}
|
|
@@ -27,6 +27,7 @@ const session_store_1 = require("./session-store");
|
|
|
27
27
|
const auth_1 = require("../server/auth");
|
|
28
28
|
const redis_1 = require("./redis");
|
|
29
29
|
const app_slug_1 = require("./app-slug");
|
|
30
|
+
const token_log_id_1 = require("./token-log-id");
|
|
30
31
|
// 5 minute threshold for "needs refresh" - matches refresh handler pattern
|
|
31
32
|
const REFRESH_THRESHOLD_MS = 5 * 60 * 1000;
|
|
32
33
|
// Concurrent refresh handling configuration
|
|
@@ -360,7 +361,7 @@ async function ensureFreshToken(request) {
|
|
|
360
361
|
}
|
|
361
362
|
}
|
|
362
363
|
console.log('[TOKEN_LIFECYCLE] ensureFreshToken check:', {
|
|
363
|
-
sessionToken:
|
|
364
|
+
sessionToken: (0, token_log_id_1.tokenLogId)(sessionToken),
|
|
364
365
|
accessTokenExpires: tokenExpiresStr,
|
|
365
366
|
now: new Date().toISOString(),
|
|
366
367
|
needsRefresh: needsRefreshNow,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tokenLogId - a log-safe correlation id for a session token (PR 154 follow-up, BAPert ruling on PAY-1427:
|
|
3
|
+
* "no token material in logs, not even partial").
|
|
4
|
+
*
|
|
5
|
+
* The package used to log `sessionToken.substring(0, 8) + '...'` so lines about one session could be
|
|
6
|
+
* correlated. Those 8 characters ARE token material. This returns `h:` + the first 12 hex characters of
|
|
7
|
+
* SHA-256(token) instead: the same correlation (one token -> one stable id, across every log line and
|
|
8
|
+
* process), with nothing of the token itself.
|
|
9
|
+
*
|
|
10
|
+
* Synchronous and dependency-free on purpose: the call sites include Edge middleware
|
|
11
|
+
* (create-middleware, auth-decision) and browser code (authStore, SessionSync), where node:crypto is
|
|
12
|
+
* unavailable and Web Crypto's digest is async. SHA-256 below follows FIPS 180-4 and is verified
|
|
13
|
+
* against node:crypto in test/session-token-log-id.test.cjs.
|
|
14
|
+
*/
|
|
15
|
+
/** SHA-256 of the UTF-8 encoding of `input`, as lowercase hex. */
|
|
16
|
+
export declare function sha256Hex(input: string): string;
|
|
17
|
+
/** `h:` + 12 hex of SHA-256(token); `none` when there is no token. Never any part of the token itself. */
|
|
18
|
+
export declare function tokenLogId(token: string | null | undefined): string;
|