@payez/next-mvp 4.4.4 → 4.4.6
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 +6 -3
- 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 +14 -0
- package/dist/auth/better-auth.js +33 -13
- package/dist/auth/utils/idp-client.js +2 -1
- 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/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 +65 -14
- package/dist/lib/idp-fetch.js +1 -1
- 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 +45 -0
- package/dist/lib/two-factor-verify-failure.js +143 -0
- package/dist/middleware/create-middleware.d.ts +2 -1
- package/dist/middleware/create-middleware.js +43 -8
- 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 +2 -1
- 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/viability.d.ts +3 -2
- package/dist/routes/auth/viability.js +11 -7
- package/dist/server/auth.js +10 -6
- 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/sessions.js +2 -1
- package/package.json +2 -1
- package/scripts/verify-release-provenance.mjs +410 -0
- package/src/api/auth-handler.ts +5 -3
- package/src/api-handlers/account/change-password.ts +6 -3
- 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 +35 -10
- 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/hooks/useViabilitySession.ts +2 -1
- 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 +90 -14
- package/src/lib/idp-fetch.ts +1 -1
- 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 +167 -0
- package/src/middleware/create-middleware.ts +55 -9
- 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 +2 -1
- package/src/routes/auth/logout.ts +4 -2
- package/src/routes/auth/session.ts +16 -10
- package/src/routes/auth/viability.ts +11 -7
- package/src/server/auth.ts +10 -6
- 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/sessions.ts +2 -1
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import 'server-only';
|
|
9
9
|
import { cookies } from 'next/headers';
|
|
10
|
-
import {
|
|
10
|
+
import { type JWTPayload } from 'jose';
|
|
11
11
|
import { getSession, type SessionData } from '../lib/session-store';
|
|
12
12
|
import { getIDPClientConfig } from '../lib/idp-client-config';
|
|
13
13
|
import { getSessionCookieName, getSecureSessionCookieName } from '../lib/app-slug';
|
|
14
14
|
import { ensureInitialized } from '../lib/startup-init';
|
|
15
15
|
import { extractSessionTokenFromCookieValue } from '../lib/session-cookie-shape';
|
|
16
|
+
import { resolveVerifiedSessionCookie } from './verified-session-token';
|
|
17
|
+
import { tokenLogId } from '../lib/token-log-id';
|
|
16
18
|
|
|
17
19
|
export interface DecodedSession {
|
|
18
20
|
sessionData: SessionData;
|
|
@@ -137,7 +139,7 @@ async function tryBetterAuthSession(
|
|
|
137
139
|
if (!idpTokens?.roles?.length) {
|
|
138
140
|
console.warn(
|
|
139
141
|
'[DECODE-SESSION] Leg 1 declining: idpTokens unusable (no roles) for session',
|
|
140
|
-
result.session.token
|
|
142
|
+
tokenLogId(result.session.token),
|
|
141
143
|
'— falling through to the sess: leg'
|
|
142
144
|
);
|
|
143
145
|
return null;
|
|
@@ -252,32 +254,20 @@ export async function decodeSession(
|
|
|
252
254
|
// death, because the ONLY fallback was structurally blind to the only
|
|
253
255
|
// shape it would ever be handed.
|
|
254
256
|
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
257
|
+
// PAY-1476: the shape still decides HOW to verify, but BOTH shapes are now
|
|
258
|
+
// verified, by the shared resolver. This branch used to take a BA-hmac
|
|
259
|
+
// cookie's token with no signature check ("Redis presence is the
|
|
260
|
+
// authority"), so `{leaked token}.{anything}` authenticated here - including
|
|
261
|
+
// behind the portal's admin API gate. The resolver checks the HMAC with
|
|
262
|
+
// timingSafeEqual (ba-hmac) or runs jwtVerify (jws) against the same
|
|
263
|
+
// env-first auth secret, and returns null on any failure.
|
|
260
264
|
const shaped = extractSessionTokenFromCookieValue(cookieValue);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (shaped?.shape === 'ba-hmac') {
|
|
266
|
-
sessionToken = shaped.token;
|
|
267
|
-
} else {
|
|
268
|
-
const secretKey = new TextEncoder().encode(secret);
|
|
269
|
-
try {
|
|
270
|
-
const result = await jwtVerify(cookieValue, secretKey);
|
|
271
|
-
payload = result.payload;
|
|
272
|
-
} catch (jwtError) {
|
|
273
|
-
console.warn('[DECODE-SESSION] JWT verification failed:', jwtError instanceof Error ? jwtError.message : String(jwtError));
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
sessionToken = (payload as any).sessionToken || (payload as any).redisSessionId;
|
|
277
|
-
}
|
|
265
|
+
const verified = await resolveVerifiedSessionCookie(cookieValue);
|
|
266
|
+
const sessionToken = verified?.token;
|
|
267
|
+
const payload: JWTPayload | undefined = verified?.payload;
|
|
278
268
|
|
|
279
269
|
if (!sessionToken) {
|
|
280
|
-
console.warn('[DECODE-SESSION]
|
|
270
|
+
console.warn('[DECODE-SESSION] Session cookie did not verify (shape:', shaped?.shape ?? 'unrecognized', ')');
|
|
281
271
|
return null;
|
|
282
272
|
}
|
|
283
273
|
|
|
@@ -292,7 +282,7 @@ export async function decodeSession(
|
|
|
292
282
|
// fixing a silent path.
|
|
293
283
|
console.warn(
|
|
294
284
|
'[DECODE-SESSION] No sess: record for token',
|
|
295
|
-
sessionToken
|
|
285
|
+
tokenLogId(sessionToken),
|
|
296
286
|
'— no fallback available, session will be signed out'
|
|
297
287
|
);
|
|
298
288
|
return null;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAY-1476 / card 372275 (R941 s1b) - THE ONE SHARED VERIFIED RESOLVER for a session token taken from a cookie.
|
|
3
|
+
*
|
|
4
|
+
* THE DEFECT (4.4.5, measured at the deployed portal by QAPert-NightHawk, mail 59030):
|
|
5
|
+
* Module A server/decode-session.ts the ba-hmac branch took `{token}.{anything}` and used segment 0 unread.
|
|
6
|
+
* Module B server/auth.ts Path B took the token from ANY recognised shape and never verified anything;
|
|
7
|
+
* a forged JWS resolved there too.
|
|
8
|
+
* Both then treated "a Redis record exists" as the validation. So a leaked session token string was a bearer
|
|
9
|
+
* credential with no signature needed, including on the portal's admin API gate (api-admin-gate -> decodeSession).
|
|
10
|
+
*
|
|
11
|
+
* THE RULE THIS MODULE ENFORCES: a cookie yields a session token ONLY after its signature checks out against
|
|
12
|
+
* the auth secret. Every caller that turns a cookie into a token for the session store awaits this function.
|
|
13
|
+
* lib/session-cookie-shape.ts stays classification-only and secret-free (it cannot verify, by construction).
|
|
14
|
+
*
|
|
15
|
+
* ba-hmac `{token}.{sig}` as Better Auth mints it (better-call signCookieValue): sig =
|
|
16
|
+
* base64(HMAC-SHA256(secret, token)), standard padding, 44 characters. Compared with
|
|
17
|
+
* crypto.timingSafeEqual over equal-length buffers.
|
|
18
|
+
* jws legacy NextAuth-era cookie: jose jwtVerify with the same secret, then the sessionToken
|
|
19
|
+
* (or redisSessionId) claim.
|
|
20
|
+
* anything else, a missing secret, or a failed check -> null. Never throws.
|
|
21
|
+
*
|
|
22
|
+
* THE SECRET is the auth secret from getIDPClientConfig(), which resolves env-first (BETTER_AUTH_SECRET,
|
|
23
|
+
* NEXTAUTH_SECRET, then IdP config; PAY-1460). The cookie HMAC key IS the auth secret, so the resolver and the
|
|
24
|
+
* config loader cannot disagree about which secret signs sessions (BAPert 59034).
|
|
25
|
+
*/
|
|
26
|
+
import { createHmac, timingSafeEqual } from 'crypto';
|
|
27
|
+
import { jwtVerify, type JWTPayload } from 'jose';
|
|
28
|
+
import { getIDPClientConfig } from '../lib/idp-client-config';
|
|
29
|
+
import { extractSessionCookieValue, extractSessionTokenFromCookieValue } from '../lib/session-cookie-shape';
|
|
30
|
+
|
|
31
|
+
/** Better Auth's signature: base64 of a 32-byte HMAC-SHA256, padded = 44 chars ending in '='. */
|
|
32
|
+
const BA_SIGNATURE_LENGTH = 44;
|
|
33
|
+
|
|
34
|
+
async function authSecret(): Promise<string | null> {
|
|
35
|
+
try {
|
|
36
|
+
const config = await getIDPClientConfig();
|
|
37
|
+
return config?.authSecret ? String(config.authSecret) : null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function verifyBaHmac(rawValue: string, secret: string): string | null {
|
|
44
|
+
const dot = rawValue.lastIndexOf('.');
|
|
45
|
+
if (dot < 1) return null;
|
|
46
|
+
const token = rawValue.slice(0, dot);
|
|
47
|
+
let signature = rawValue.slice(dot + 1);
|
|
48
|
+
if (signature.includes('%')) {
|
|
49
|
+
try {
|
|
50
|
+
signature = decodeURIComponent(signature);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (signature.length !== BA_SIGNATURE_LENGTH || !signature.endsWith('=')) return null;
|
|
56
|
+
const expected = createHmac('sha256', secret).update(token, 'utf8').digest('base64');
|
|
57
|
+
const a = Buffer.from(signature, 'utf8');
|
|
58
|
+
const b = Buffer.from(expected, 'utf8');
|
|
59
|
+
if (a.length !== b.length) return null;
|
|
60
|
+
return timingSafeEqual(a, b) ? token : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface VerifiedSessionCookie {
|
|
64
|
+
token: string;
|
|
65
|
+
shape: 'ba-hmac' | 'jws';
|
|
66
|
+
/** The verified JWS payload (jws shape only). */
|
|
67
|
+
payload?: JWTPayload;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function verifyJws(rawValue: string, secret: string): Promise<VerifiedSessionCookie | null> {
|
|
71
|
+
try {
|
|
72
|
+
const { payload } = await jwtVerify(rawValue, new TextEncoder().encode(secret));
|
|
73
|
+
const claim = (payload as Record<string, unknown>).sessionToken ?? (payload as Record<string, unknown>).redisSessionId;
|
|
74
|
+
return typeof claim === 'string' && claim ? { token: claim, shape: 'jws', payload } : null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The verified session cookie (token, shape, JWS payload), or null unless its signature verifies. */
|
|
81
|
+
export async function resolveVerifiedSessionCookie(rawValue: string | null | undefined): Promise<VerifiedSessionCookie | null> {
|
|
82
|
+
if (!rawValue) return null;
|
|
83
|
+
const shaped = extractSessionTokenFromCookieValue(rawValue);
|
|
84
|
+
if (!shaped) return null;
|
|
85
|
+
const secret = await authSecret();
|
|
86
|
+
if (!secret) return null;
|
|
87
|
+
if (shaped.shape === 'ba-hmac') {
|
|
88
|
+
const token = verifyBaHmac(rawValue, secret);
|
|
89
|
+
return token ? { token, shape: 'ba-hmac' } : null;
|
|
90
|
+
}
|
|
91
|
+
return verifyJws(rawValue, secret);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The session token carried by a raw session-cookie value, or null unless its signature verifies. */
|
|
95
|
+
export async function resolveVerifiedSessionToken(rawValue: string | null | undefined): Promise<string | null> {
|
|
96
|
+
const verified = await resolveVerifiedSessionCookie(rawValue);
|
|
97
|
+
return verified ? verified.token : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Same, reading the session cookie (every name it is minted under, chunks joined) from a Request. */
|
|
101
|
+
export async function resolveVerifiedSessionTokenFromRequest(request: Request): Promise<string | null> {
|
|
102
|
+
return resolveVerifiedSessionToken(extractSessionCookieValue(request));
|
|
103
|
+
}
|
package/src/stores/authStore.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
getCsrfCookieName,
|
|
27
27
|
getSecureCsrfCookieName
|
|
28
28
|
} from '../lib/app-slug';
|
|
29
|
+
import { tokenLogId } from '../lib/token-log-id';
|
|
29
30
|
|
|
30
31
|
// ===============================
|
|
31
32
|
// INTERFACES & TYPES
|
|
@@ -484,19 +485,28 @@ export const useAuthStore = create<AuthStore>()(
|
|
|
484
485
|
|
|
485
486
|
forceLogoutAndRedirect: async (reason: string) => {
|
|
486
487
|
const state = get();
|
|
487
|
-
|
|
488
|
+
// Same truncation convention as the server-side session logs (session-store.ts,
|
|
489
|
+
// refresh.ts): a session token is a bearer credential, and this line goes to the
|
|
490
|
+
// browser console (and whatever aggregates authLogger client-side) - full value
|
|
491
|
+
// was leftover debug logging (DotNetPert-Scout, mail 58678).
|
|
492
|
+
// PAY-1505: the ternary that used to stand here fell back to the RAW token on its false arm.
|
|
493
|
+
// Safe only by the unenforced property that the arm is reached when the token is falsy -
|
|
494
|
+
// exactly the shape the source guard exists to refuse. tokenLogId already answers 'none'
|
|
495
|
+
// for an absent token, so the fallback is not needed.
|
|
496
|
+
const sessionTokenPrefix = tokenLogId(state.session?.sessionToken);
|
|
497
|
+
|
|
488
498
|
// AGGRESSIVE LOGGING TO TRACK EXECUTION
|
|
489
499
|
console.error('🚨 FORCE LOGOUT INITIATED 🚨', {
|
|
490
500
|
reason,
|
|
491
501
|
userId: state.user?.id,
|
|
492
|
-
sessionToken:
|
|
502
|
+
sessionToken: sessionTokenPrefix,
|
|
493
503
|
timestamp: new Date().toISOString()
|
|
494
504
|
});
|
|
495
|
-
|
|
505
|
+
|
|
496
506
|
authLogger.error('[AuthStore] Force logout initiated', {
|
|
497
507
|
reason,
|
|
498
508
|
userId: state.user?.id,
|
|
499
|
-
sessionToken:
|
|
509
|
+
sessionToken: sessionTokenPrefix
|
|
500
510
|
});
|
|
501
511
|
|
|
502
512
|
try {
|
|
@@ -1870,7 +1880,8 @@ export const useAuthStore = create<AuthStore>()(
|
|
|
1870
1880
|
|
|
1871
1881
|
// If this event is about the current user, update their state
|
|
1872
1882
|
if (data.userId === state.user?.id && state.session) {
|
|
1873
|
-
|
|
1883
|
+
// PAY-1427: the event is user state; log its shape, not its contents.
|
|
1884
|
+
authLogger.info('[AuthStore] Updating current user state from SignalR:', { keys: Object.keys(data || {}) });
|
|
1874
1885
|
// ... user state update logic would go here
|
|
1875
1886
|
}
|
|
1876
1887
|
},
|
package/src/types/recovery.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type RecoveryStep = 'initiate' | 'select-method' | 'verify-code' | 'set-password' | 'complete';
|
|
1
|
+
export type RecoveryStep = 'initiate' | 'select-method' | 'verify-code' | 'set-password' | 'requested' | 'complete';
|
|
2
2
|
|
|
3
3
|
export interface RecoverySession {
|
|
4
4
|
recoveryToken: string;
|
package/src/utils/api.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ENV_CONFIG, API_ENDPOINTS } from '../config/env';
|
|
2
2
|
import { TwoFactorMethod } from '../types/security';
|
|
3
3
|
import { standardizedApi, isApiSuccess, isApiError, extractApiData } from '../lib/standardized-client-api';
|
|
4
|
+
import { knownPasswordRules, passwordRuleSentences } from '../lib/password-rules';
|
|
4
5
|
|
|
5
6
|
export class ApiError extends Error {
|
|
6
7
|
type: string;
|
|
@@ -96,6 +97,35 @@ export async function apiFetch<T>(url: string, options: ApiOptions = {}): Promis
|
|
|
96
97
|
// DELETED: apiFetchAuth - Use clientApi from @/lib/client-api instead for proper token refresh handling
|
|
97
98
|
|
|
98
99
|
// Account API for masked info and 2FA
|
|
100
|
+
/**
|
|
101
|
+
* PAY-1591: what the user reads when the LAST recovery step fails. Four outcomes, told apart only by codes the IdP really sends
|
|
102
|
+
* (see resetFailureText): a password-policy refusal names the failed rules; an explicit expired/used token says so and links back
|
|
103
|
+
* to recovery; any other 400/422 gets a neutral 'check the requirements and try again' (the user's recovery session is kept); and
|
|
104
|
+
* a server or transport failure gets a retry sentence. None of them is the old fixed "Failed to reset password".
|
|
105
|
+
*/
|
|
106
|
+
export const RECOVERY_EXPIRED_TEXT = 'This reset link has expired or was already used. Start password recovery again.';
|
|
107
|
+
|
|
108
|
+
export const RESET_NEUTRAL_TEXT = "We couldn't set that password. Check it meets the requirements shown and try again.";
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Three tiers for a 400/422 from reset (rigpert 60822, QAPert 60820), decided ONLY on codes the IdP actually sends:
|
|
112
|
+
* policy details.code password_policy (projected by the handler as password_policy) -> the rule sentences
|
|
113
|
+
* tokenInvalid an EXPLICIT expired/used signal (token_expired / token_used / Identity InvalidToken, projected as
|
|
114
|
+
* reset_token_invalid) -> 'link expired, start again'
|
|
115
|
+
* anything else -> NEUTRAL, which must NOT tell the user their link is dead: the served IdP sends a weak-password refusal with
|
|
116
|
+
* no code, and 'expired' there loops them after they spent their emailed code. The recovery session is kept, so
|
|
117
|
+
* they can retry.
|
|
118
|
+
*/
|
|
119
|
+
export function resetFailureText(status: number, rules: string[] = [], policy = false, tokenInvalid = false): string {
|
|
120
|
+
if (status === 400 || status === 422) {
|
|
121
|
+
if (tokenInvalid && !policy) return RECOVERY_EXPIRED_TEXT;
|
|
122
|
+
if (!policy) return RESET_NEUTRAL_TEXT;
|
|
123
|
+
const base = "That password wasn't accepted.";
|
|
124
|
+
return rules.length ? `${base} ${passwordRuleSentences(rules)}` : `${base} Try a different one; the rules are shown above.`;
|
|
125
|
+
}
|
|
126
|
+
return "We couldn't reset your password just now. Please try again.";
|
|
127
|
+
}
|
|
128
|
+
|
|
99
129
|
export class AccountApi {
|
|
100
130
|
/**
|
|
101
131
|
* Get masked user info (email, phone, authenticator status)
|
|
@@ -116,18 +146,16 @@ export class AccountApi {
|
|
|
116
146
|
// Empty body: user info comes from JWT token
|
|
117
147
|
const result = await standardizedApi.post<any>('/api/account/masked-info', {}, accessToken);
|
|
118
148
|
|
|
149
|
+
// PAY-1427: masked email/phone are still user data - log the response shape, never its values.
|
|
119
150
|
console.log('[DEBUG] getMaskedInfo result:', {
|
|
120
151
|
success: isApiSuccess(result),
|
|
121
152
|
hasData: isApiSuccess(result) && !!result.data,
|
|
122
|
-
rawData: isApiSuccess(result) ? result.data : null,
|
|
123
153
|
dataFields: isApiSuccess(result) && result.data ? Object.keys(result.data) : []
|
|
124
154
|
});
|
|
125
155
|
|
|
126
|
-
console.log('[DEBUG] getMaskedInfo raw data structure:', JSON.stringify(isApiSuccess(result) ? result.data : result, null, 2));
|
|
127
|
-
|
|
128
156
|
if (!isApiSuccess(result)) {
|
|
129
157
|
const errorMessage = isApiError(result) ? result.message : 'Failed to get masked info';
|
|
130
|
-
console.error('[DEBUG] getMaskedInfo failed:', result);
|
|
158
|
+
console.error('[DEBUG] getMaskedInfo failed:', { keys: result && typeof result === 'object' ? Object.keys(result) : typeof result });
|
|
131
159
|
throw new ApiError(errorMessage, result);
|
|
132
160
|
}
|
|
133
161
|
// Use only snake_case fields from backend response
|
|
@@ -149,6 +177,8 @@ export class AccountApi {
|
|
|
149
177
|
headers: { 'Content-Type': 'application/json' },
|
|
150
178
|
body: JSON.stringify({ email }),
|
|
151
179
|
});
|
|
180
|
+
// PAY-1588: a throttled caller (429) must look exactly like an unknown one - no session, no distinguishing error.
|
|
181
|
+
if (res.status === 429) return { success: false, data: {} } as any;
|
|
152
182
|
if (!res.ok) throw new Error('Failed to initiate recovery');
|
|
153
183
|
return res.json();
|
|
154
184
|
}
|
|
@@ -193,12 +223,21 @@ export class AccountApi {
|
|
|
193
223
|
headers: { 'Content-Type': 'application/json' },
|
|
194
224
|
body: JSON.stringify({
|
|
195
225
|
email,
|
|
196
|
-
|
|
226
|
+
token: resetToken,
|
|
197
227
|
new_password: newPassword,
|
|
198
228
|
confirm_password: confirmPassword,
|
|
199
229
|
}),
|
|
200
230
|
});
|
|
201
|
-
if (!res.ok)
|
|
231
|
+
if (!res.ok) {
|
|
232
|
+
const body = await res.json().catch(() => null);
|
|
233
|
+
const policy = !!body?.password_policy;
|
|
234
|
+
const rules = knownPasswordRules(body?.password_policy?.rules);
|
|
235
|
+
const tokenInvalid = body?.reset_token_invalid === true;
|
|
236
|
+
const err = new Error(resetFailureText(res.status, rules, policy, tokenInvalid)) as Error & { rules?: string[]; code?: string };
|
|
237
|
+
if (rules.length) err.rules = rules;
|
|
238
|
+
if ((res.status === 400 || res.status === 422) && tokenInvalid && !policy) err.code = 'RECOVERY_EXPIRED';
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
202
241
|
return res.json();
|
|
203
242
|
}
|
|
204
243
|
|
|
@@ -210,8 +249,8 @@ export class AccountApi {
|
|
|
210
249
|
body: JSON.stringify({
|
|
211
250
|
email: data.email,
|
|
212
251
|
token: data.token,
|
|
213
|
-
|
|
214
|
-
|
|
252
|
+
new_password: data.password,
|
|
253
|
+
confirm_password: data.password_confirmation
|
|
215
254
|
})
|
|
216
255
|
})
|
|
217
256
|
const result = await res.json().catch(() => ({}))
|
package/src/vibe/sessions.ts
CHANGED
|
@@ -116,7 +116,8 @@ async function vibeProxyRequest<T>(
|
|
|
116
116
|
|
|
117
117
|
if (!response.ok) {
|
|
118
118
|
const errorText = await response.text();
|
|
119
|
-
|
|
119
|
+
// PAY-1427: never the raw proxy body - the request carried a Bearer token and an error page can echo it.
|
|
120
|
+
console.error(`[vibe-sessions] Proxy error ${response.status}:`, { bodyLength: errorText.length });
|
|
120
121
|
return null;
|
|
121
122
|
}
|
|
122
123
|
|