@payez/next-mvp 4.4.5 → 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 +35 -49
- 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 +66 -45
- 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 +37 -55
- 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 +91 -45
- 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
package/dist/server/auth.js
CHANGED
|
@@ -49,7 +49,7 @@ require("server-only");
|
|
|
49
49
|
const better_auth_1 = require("../auth/better-auth");
|
|
50
50
|
const idp_client_config_1 = require("../lib/idp-client-config");
|
|
51
51
|
const session_store_1 = require("../lib/session-store");
|
|
52
|
-
const
|
|
52
|
+
const verified_session_token_1 = require("./verified-session-token");
|
|
53
53
|
const single_flight_1 = require("../lib/single-flight");
|
|
54
54
|
let authInstance = null;
|
|
55
55
|
function buildSessionDataFromAuthSession(session) {
|
|
@@ -184,14 +184,18 @@ async function getSession(request) {
|
|
|
184
184
|
sessionData = buildSessionDataFromAuthSession(betterAuthSession);
|
|
185
185
|
return attachSessionData(betterAuthSession, sessionData, sessionToken);
|
|
186
186
|
}
|
|
187
|
-
// Path B — Better Auth said null/incomplete. Fall back to
|
|
187
|
+
// Path B — Better Auth said null/incomplete. Fall back to a VERIFIED cookie
|
|
188
188
|
// decode + Redis-canonical liveness check. This catches cases where the
|
|
189
189
|
// canonical app session at `{slug}:{token}` is still alive but Better
|
|
190
190
|
// Auth's `ba:{slug}:{token}` secondary record was evicted or the
|
|
191
|
-
// in-memory cookie cache returned stale-null.
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
191
|
+
// in-memory cookie cache returned stale-null.
|
|
192
|
+
//
|
|
193
|
+
// PAY-1476: this path used to take the token with NO signature check, on the
|
|
194
|
+
// theory that "Redis lookup is the validation". It is not: a leaked token has
|
|
195
|
+
// a live record, so the token string alone authenticated, with any garbage in
|
|
196
|
+
// the signature position, and a forged JWS too. The token now comes only from
|
|
197
|
+
// the shared verified resolver (HMAC or JWS signature checked first).
|
|
198
|
+
const sessionToken = await (0, verified_session_token_1.resolveVerifiedSessionTokenFromRequest)(request);
|
|
195
199
|
if (!sessionToken)
|
|
196
200
|
return null;
|
|
197
201
|
let sessionData = null;
|
|
@@ -42,12 +42,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
42
42
|
exports.decodeSession = decodeSession;
|
|
43
43
|
require("server-only");
|
|
44
44
|
const headers_1 = require("next/headers");
|
|
45
|
-
const jose_1 = require("jose");
|
|
46
45
|
const session_store_1 = require("../lib/session-store");
|
|
47
46
|
const idp_client_config_1 = require("../lib/idp-client-config");
|
|
48
47
|
const app_slug_1 = require("../lib/app-slug");
|
|
49
48
|
const startup_init_1 = require("../lib/startup-init");
|
|
50
49
|
const session_cookie_shape_1 = require("../lib/session-cookie-shape");
|
|
50
|
+
const verified_session_token_1 = require("./verified-session-token");
|
|
51
|
+
const token_log_id_1 = require("../lib/token-log-id");
|
|
51
52
|
/**
|
|
52
53
|
* Try Better Auth's server-side session API.
|
|
53
54
|
* Returns a DecodedSession if Better Auth has an active session, null otherwise.
|
|
@@ -161,7 +162,7 @@ async function tryBetterAuthSession(requestCookies) {
|
|
|
161
162
|
// session. Read the commit message before treating a sign-out here as a
|
|
162
163
|
// bug introduced by this fix.
|
|
163
164
|
if (!idpTokens?.roles?.length) {
|
|
164
|
-
console.warn('[DECODE-SESSION] Leg 1 declining: idpTokens unusable (no roles) for session', result.session.token
|
|
165
|
+
console.warn('[DECODE-SESSION] Leg 1 declining: idpTokens unusable (no roles) for session', (0, token_log_id_1.tokenLogId)(result.session.token), '— falling through to the sess: leg');
|
|
165
166
|
return null;
|
|
166
167
|
}
|
|
167
168
|
// Map Better Auth session + IDP tokens to SessionData
|
|
@@ -261,31 +262,19 @@ async function decodeSession(requestCookies) {
|
|
|
261
262
|
// death, because the ONLY fallback was structurally blind to the only
|
|
262
263
|
// shape it would ever be handed.
|
|
263
264
|
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
265
|
+
// PAY-1476: the shape still decides HOW to verify, but BOTH shapes are now
|
|
266
|
+
// verified, by the shared resolver. This branch used to take a BA-hmac
|
|
267
|
+
// cookie's token with no signature check ("Redis presence is the
|
|
268
|
+
// authority"), so `{leaked token}.{anything}` authenticated here - including
|
|
269
|
+
// behind the portal's admin API gate. The resolver checks the HMAC with
|
|
270
|
+
// timingSafeEqual (ba-hmac) or runs jwtVerify (jws) against the same
|
|
271
|
+
// env-first auth secret, and returns null on any failure.
|
|
269
272
|
const shaped = (0, session_cookie_shape_1.extractSessionTokenFromCookieValue)(cookieValue);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
sessionToken = shaped.token;
|
|
274
|
-
}
|
|
275
|
-
else {
|
|
276
|
-
const secretKey = new TextEncoder().encode(secret);
|
|
277
|
-
try {
|
|
278
|
-
const result = await (0, jose_1.jwtVerify)(cookieValue, secretKey);
|
|
279
|
-
payload = result.payload;
|
|
280
|
-
}
|
|
281
|
-
catch (jwtError) {
|
|
282
|
-
console.warn('[DECODE-SESSION] JWT verification failed:', jwtError instanceof Error ? jwtError.message : String(jwtError));
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
285
|
-
sessionToken = payload.sessionToken || payload.redisSessionId;
|
|
286
|
-
}
|
|
273
|
+
const verified = await (0, verified_session_token_1.resolveVerifiedSessionCookie)(cookieValue);
|
|
274
|
+
const sessionToken = verified?.token;
|
|
275
|
+
const payload = verified?.payload;
|
|
287
276
|
if (!sessionToken) {
|
|
288
|
-
console.warn('[DECODE-SESSION]
|
|
277
|
+
console.warn('[DECODE-SESSION] Session cookie did not verify (shape:', shaped?.shape ?? 'unrecognized', ')');
|
|
289
278
|
return null;
|
|
290
279
|
}
|
|
291
280
|
const sessionData = await (0, session_store_1.getSession)(sessionToken);
|
|
@@ -297,7 +286,7 @@ async function decodeSession(requestCookies) {
|
|
|
297
286
|
// either and the caller is about to be signed out. Different meanings,
|
|
298
287
|
// different remediations — do not ship a second silent path while
|
|
299
288
|
// fixing a silent path.
|
|
300
|
-
console.warn('[DECODE-SESSION] No sess: record for token',
|
|
289
|
+
console.warn('[DECODE-SESSION] No sess: record for token', (0, token_log_id_1.tokenLogId)(sessionToken), '— no fallback available, session will be signed out');
|
|
301
290
|
return null;
|
|
302
291
|
}
|
|
303
292
|
// Backwards compat: session.user.email works alongside session.email
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type JWTPayload } from 'jose';
|
|
2
|
+
export interface VerifiedSessionCookie {
|
|
3
|
+
token: string;
|
|
4
|
+
shape: 'ba-hmac' | 'jws';
|
|
5
|
+
/** The verified JWS payload (jws shape only). */
|
|
6
|
+
payload?: JWTPayload;
|
|
7
|
+
}
|
|
8
|
+
/** The verified session cookie (token, shape, JWS payload), or null unless its signature verifies. */
|
|
9
|
+
export declare function resolveVerifiedSessionCookie(rawValue: string | null | undefined): Promise<VerifiedSessionCookie | null>;
|
|
10
|
+
/** The session token carried by a raw session-cookie value, or null unless its signature verifies. */
|
|
11
|
+
export declare function resolveVerifiedSessionToken(rawValue: string | null | undefined): Promise<string | null>;
|
|
12
|
+
/** Same, reading the session cookie (every name it is minted under, chunks joined) from a Request. */
|
|
13
|
+
export declare function resolveVerifiedSessionTokenFromRequest(request: Request): Promise<string | null>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveVerifiedSessionCookie = resolveVerifiedSessionCookie;
|
|
4
|
+
exports.resolveVerifiedSessionToken = resolveVerifiedSessionToken;
|
|
5
|
+
exports.resolveVerifiedSessionTokenFromRequest = resolveVerifiedSessionTokenFromRequest;
|
|
6
|
+
/**
|
|
7
|
+
* PAY-1476 / card 372275 (R941 s1b) - THE ONE SHARED VERIFIED RESOLVER for a session token taken from a cookie.
|
|
8
|
+
*
|
|
9
|
+
* THE DEFECT (4.4.5, measured at the deployed portal by QAPert-NightHawk, mail 59030):
|
|
10
|
+
* Module A server/decode-session.ts the ba-hmac branch took `{token}.{anything}` and used segment 0 unread.
|
|
11
|
+
* Module B server/auth.ts Path B took the token from ANY recognised shape and never verified anything;
|
|
12
|
+
* a forged JWS resolved there too.
|
|
13
|
+
* Both then treated "a Redis record exists" as the validation. So a leaked session token string was a bearer
|
|
14
|
+
* credential with no signature needed, including on the portal's admin API gate (api-admin-gate -> decodeSession).
|
|
15
|
+
*
|
|
16
|
+
* THE RULE THIS MODULE ENFORCES: a cookie yields a session token ONLY after its signature checks out against
|
|
17
|
+
* the auth secret. Every caller that turns a cookie into a token for the session store awaits this function.
|
|
18
|
+
* lib/session-cookie-shape.ts stays classification-only and secret-free (it cannot verify, by construction).
|
|
19
|
+
*
|
|
20
|
+
* ba-hmac `{token}.{sig}` as Better Auth mints it (better-call signCookieValue): sig =
|
|
21
|
+
* base64(HMAC-SHA256(secret, token)), standard padding, 44 characters. Compared with
|
|
22
|
+
* crypto.timingSafeEqual over equal-length buffers.
|
|
23
|
+
* jws legacy NextAuth-era cookie: jose jwtVerify with the same secret, then the sessionToken
|
|
24
|
+
* (or redisSessionId) claim.
|
|
25
|
+
* anything else, a missing secret, or a failed check -> null. Never throws.
|
|
26
|
+
*
|
|
27
|
+
* THE SECRET is the auth secret from getIDPClientConfig(), which resolves env-first (BETTER_AUTH_SECRET,
|
|
28
|
+
* NEXTAUTH_SECRET, then IdP config; PAY-1460). The cookie HMAC key IS the auth secret, so the resolver and the
|
|
29
|
+
* config loader cannot disagree about which secret signs sessions (BAPert 59034).
|
|
30
|
+
*/
|
|
31
|
+
const crypto_1 = require("crypto");
|
|
32
|
+
const jose_1 = require("jose");
|
|
33
|
+
const idp_client_config_1 = require("../lib/idp-client-config");
|
|
34
|
+
const session_cookie_shape_1 = require("../lib/session-cookie-shape");
|
|
35
|
+
/** Better Auth's signature: base64 of a 32-byte HMAC-SHA256, padded = 44 chars ending in '='. */
|
|
36
|
+
const BA_SIGNATURE_LENGTH = 44;
|
|
37
|
+
async function authSecret() {
|
|
38
|
+
try {
|
|
39
|
+
const config = await (0, idp_client_config_1.getIDPClientConfig)();
|
|
40
|
+
return config?.authSecret ? String(config.authSecret) : null;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function verifyBaHmac(rawValue, secret) {
|
|
47
|
+
const dot = rawValue.lastIndexOf('.');
|
|
48
|
+
if (dot < 1)
|
|
49
|
+
return null;
|
|
50
|
+
const token = rawValue.slice(0, dot);
|
|
51
|
+
let signature = rawValue.slice(dot + 1);
|
|
52
|
+
if (signature.includes('%')) {
|
|
53
|
+
try {
|
|
54
|
+
signature = decodeURIComponent(signature);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (signature.length !== BA_SIGNATURE_LENGTH || !signature.endsWith('='))
|
|
61
|
+
return null;
|
|
62
|
+
const expected = (0, crypto_1.createHmac)('sha256', secret).update(token, 'utf8').digest('base64');
|
|
63
|
+
const a = Buffer.from(signature, 'utf8');
|
|
64
|
+
const b = Buffer.from(expected, 'utf8');
|
|
65
|
+
if (a.length !== b.length)
|
|
66
|
+
return null;
|
|
67
|
+
return (0, crypto_1.timingSafeEqual)(a, b) ? token : null;
|
|
68
|
+
}
|
|
69
|
+
async function verifyJws(rawValue, secret) {
|
|
70
|
+
try {
|
|
71
|
+
const { payload } = await (0, jose_1.jwtVerify)(rawValue, new TextEncoder().encode(secret));
|
|
72
|
+
const claim = payload.sessionToken ?? payload.redisSessionId;
|
|
73
|
+
return typeof claim === 'string' && claim ? { token: claim, shape: 'jws', payload } : null;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** The verified session cookie (token, shape, JWS payload), or null unless its signature verifies. */
|
|
80
|
+
async function resolveVerifiedSessionCookie(rawValue) {
|
|
81
|
+
if (!rawValue)
|
|
82
|
+
return null;
|
|
83
|
+
const shaped = (0, session_cookie_shape_1.extractSessionTokenFromCookieValue)(rawValue);
|
|
84
|
+
if (!shaped)
|
|
85
|
+
return null;
|
|
86
|
+
const secret = await authSecret();
|
|
87
|
+
if (!secret)
|
|
88
|
+
return null;
|
|
89
|
+
if (shaped.shape === 'ba-hmac') {
|
|
90
|
+
const token = verifyBaHmac(rawValue, secret);
|
|
91
|
+
return token ? { token, shape: 'ba-hmac' } : null;
|
|
92
|
+
}
|
|
93
|
+
return verifyJws(rawValue, secret);
|
|
94
|
+
}
|
|
95
|
+
/** The session token carried by a raw session-cookie value, or null unless its signature verifies. */
|
|
96
|
+
async function resolveVerifiedSessionToken(rawValue) {
|
|
97
|
+
const verified = await resolveVerifiedSessionCookie(rawValue);
|
|
98
|
+
return verified ? verified.token : null;
|
|
99
|
+
}
|
|
100
|
+
/** Same, reading the session cookie (every name it is minted under, chunks joined) from a Request. */
|
|
101
|
+
async function resolveVerifiedSessionTokenFromRequest(request) {
|
|
102
|
+
return resolveVerifiedSessionToken((0, session_cookie_shape_1.extractSessionCookieValue)(request));
|
|
103
|
+
}
|
package/dist/stores/authStore.js
CHANGED
|
@@ -55,6 +55,7 @@ const logger_1 = require("../config/logger");
|
|
|
55
55
|
const signalr_1 = require("@microsoft/signalr");
|
|
56
56
|
const signalrActivityService_1 = require("../services/signalrActivityService");
|
|
57
57
|
const app_slug_1 = require("../lib/app-slug");
|
|
58
|
+
const token_log_id_1 = require("../lib/token-log-id");
|
|
58
59
|
// ===============================
|
|
59
60
|
// CACHE CONFIGURATION
|
|
60
61
|
// ===============================
|
|
@@ -303,17 +304,26 @@ exports.useAuthStore = (0, zustand_1.create)()((0, middleware_1.devtools)((set,
|
|
|
303
304
|
},
|
|
304
305
|
forceLogoutAndRedirect: async (reason) => {
|
|
305
306
|
const state = get();
|
|
307
|
+
// Same truncation convention as the server-side session logs (session-store.ts,
|
|
308
|
+
// refresh.ts): a session token is a bearer credential, and this line goes to the
|
|
309
|
+
// browser console (and whatever aggregates authLogger client-side) - full value
|
|
310
|
+
// was leftover debug logging (DotNetPert-Scout, mail 58678).
|
|
311
|
+
// PAY-1505: the ternary that used to stand here fell back to the RAW token on its false arm.
|
|
312
|
+
// Safe only by the unenforced property that the arm is reached when the token is falsy -
|
|
313
|
+
// exactly the shape the source guard exists to refuse. tokenLogId already answers 'none'
|
|
314
|
+
// for an absent token, so the fallback is not needed.
|
|
315
|
+
const sessionTokenPrefix = (0, token_log_id_1.tokenLogId)(state.session?.sessionToken);
|
|
306
316
|
// AGGRESSIVE LOGGING TO TRACK EXECUTION
|
|
307
317
|
console.error('🚨 FORCE LOGOUT INITIATED 🚨', {
|
|
308
318
|
reason,
|
|
309
319
|
userId: state.user?.id,
|
|
310
|
-
sessionToken:
|
|
320
|
+
sessionToken: sessionTokenPrefix,
|
|
311
321
|
timestamp: new Date().toISOString()
|
|
312
322
|
});
|
|
313
323
|
logger_1.authLogger.error('[AuthStore] Force logout initiated', {
|
|
314
324
|
reason,
|
|
315
325
|
userId: state.user?.id,
|
|
316
|
-
sessionToken:
|
|
326
|
+
sessionToken: sessionTokenPrefix
|
|
317
327
|
});
|
|
318
328
|
try {
|
|
319
329
|
// TODO: Step 1: Mark session as force-invalidated in Redis (temp disabled for client build)
|
|
@@ -1503,7 +1513,8 @@ exports.useAuthStore = (0, zustand_1.create)()((0, middleware_1.devtools)((set,
|
|
|
1503
1513
|
const state = get();
|
|
1504
1514
|
// If this event is about the current user, update their state
|
|
1505
1515
|
if (data.userId === state.user?.id && state.session) {
|
|
1506
|
-
|
|
1516
|
+
// PAY-1427: the event is user state; log its shape, not its contents.
|
|
1517
|
+
logger_1.authLogger.info('[AuthStore] Updating current user state from SignalR:', { keys: Object.keys(data || {}) });
|
|
1507
1518
|
// ... user state update logic would go here
|
|
1508
1519
|
}
|
|
1509
1520
|
},
|
package/dist/types/recovery.d.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
|
export interface RecoverySession {
|
|
3
3
|
recoveryToken: string;
|
|
4
4
|
email: string;
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -25,6 +25,24 @@ export interface ApiResponse<T> {
|
|
|
25
25
|
message?: string;
|
|
26
26
|
}
|
|
27
27
|
export declare function apiFetch<T>(url: string, options?: ApiOptions): Promise<ApiResponse<T>>;
|
|
28
|
+
/**
|
|
29
|
+
* PAY-1591: what the user reads when the LAST recovery step fails. Four outcomes, told apart only by codes the IdP really sends
|
|
30
|
+
* (see resetFailureText): a password-policy refusal names the failed rules; an explicit expired/used token says so and links back
|
|
31
|
+
* to recovery; any other 400/422 gets a neutral 'check the requirements and try again' (the user's recovery session is kept); and
|
|
32
|
+
* a server or transport failure gets a retry sentence. None of them is the old fixed "Failed to reset password".
|
|
33
|
+
*/
|
|
34
|
+
export declare const RECOVERY_EXPIRED_TEXT = "This reset link has expired or was already used. Start password recovery again.";
|
|
35
|
+
export declare const RESET_NEUTRAL_TEXT = "We couldn't set that password. Check it meets the requirements shown and try again.";
|
|
36
|
+
/**
|
|
37
|
+
* Three tiers for a 400/422 from reset (rigpert 60822, QAPert 60820), decided ONLY on codes the IdP actually sends:
|
|
38
|
+
* policy details.code password_policy (projected by the handler as password_policy) -> the rule sentences
|
|
39
|
+
* tokenInvalid an EXPLICIT expired/used signal (token_expired / token_used / Identity InvalidToken, projected as
|
|
40
|
+
* reset_token_invalid) -> 'link expired, start again'
|
|
41
|
+
* anything else -> NEUTRAL, which must NOT tell the user their link is dead: the served IdP sends a weak-password refusal with
|
|
42
|
+
* no code, and 'expired' there loops them after they spent their emailed code. The recovery session is kept, so
|
|
43
|
+
* they can retry.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resetFailureText(status: number, rules?: string[], policy?: boolean, tokenInvalid?: boolean): string;
|
|
28
46
|
export declare class AccountApi {
|
|
29
47
|
/**
|
|
30
48
|
* Get masked user info (email, phone, authenticator status)
|
package/dist/utils/api.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.accountApi = exports.AccountApi = exports.ApiError = void 0;
|
|
3
|
+
exports.accountApi = exports.AccountApi = exports.RESET_NEUTRAL_TEXT = exports.RECOVERY_EXPIRED_TEXT = exports.ApiError = void 0;
|
|
4
4
|
exports.apiFetch = apiFetch;
|
|
5
|
+
exports.resetFailureText = resetFailureText;
|
|
5
6
|
const standardized_client_api_1 = require("../lib/standardized-client-api");
|
|
7
|
+
const password_rules_1 = require("../lib/password-rules");
|
|
6
8
|
class ApiError extends Error {
|
|
7
9
|
type;
|
|
8
10
|
title;
|
|
@@ -74,6 +76,34 @@ async function apiFetch(url, options = {}) {
|
|
|
74
76
|
}
|
|
75
77
|
// DELETED: apiFetchAuth - Use clientApi from @/lib/client-api instead for proper token refresh handling
|
|
76
78
|
// Account API for masked info and 2FA
|
|
79
|
+
/**
|
|
80
|
+
* PAY-1591: what the user reads when the LAST recovery step fails. Four outcomes, told apart only by codes the IdP really sends
|
|
81
|
+
* (see resetFailureText): a password-policy refusal names the failed rules; an explicit expired/used token says so and links back
|
|
82
|
+
* to recovery; any other 400/422 gets a neutral 'check the requirements and try again' (the user's recovery session is kept); and
|
|
83
|
+
* a server or transport failure gets a retry sentence. None of them is the old fixed "Failed to reset password".
|
|
84
|
+
*/
|
|
85
|
+
exports.RECOVERY_EXPIRED_TEXT = 'This reset link has expired or was already used. Start password recovery again.';
|
|
86
|
+
exports.RESET_NEUTRAL_TEXT = "We couldn't set that password. Check it meets the requirements shown and try again.";
|
|
87
|
+
/**
|
|
88
|
+
* Three tiers for a 400/422 from reset (rigpert 60822, QAPert 60820), decided ONLY on codes the IdP actually sends:
|
|
89
|
+
* policy details.code password_policy (projected by the handler as password_policy) -> the rule sentences
|
|
90
|
+
* tokenInvalid an EXPLICIT expired/used signal (token_expired / token_used / Identity InvalidToken, projected as
|
|
91
|
+
* reset_token_invalid) -> 'link expired, start again'
|
|
92
|
+
* anything else -> NEUTRAL, which must NOT tell the user their link is dead: the served IdP sends a weak-password refusal with
|
|
93
|
+
* no code, and 'expired' there loops them after they spent their emailed code. The recovery session is kept, so
|
|
94
|
+
* they can retry.
|
|
95
|
+
*/
|
|
96
|
+
function resetFailureText(status, rules = [], policy = false, tokenInvalid = false) {
|
|
97
|
+
if (status === 400 || status === 422) {
|
|
98
|
+
if (tokenInvalid && !policy)
|
|
99
|
+
return exports.RECOVERY_EXPIRED_TEXT;
|
|
100
|
+
if (!policy)
|
|
101
|
+
return exports.RESET_NEUTRAL_TEXT;
|
|
102
|
+
const base = "That password wasn't accepted.";
|
|
103
|
+
return rules.length ? `${base} ${(0, password_rules_1.passwordRuleSentences)(rules)}` : `${base} Try a different one; the rules are shown above.`;
|
|
104
|
+
}
|
|
105
|
+
return "We couldn't reset your password just now. Please try again.";
|
|
106
|
+
}
|
|
77
107
|
class AccountApi {
|
|
78
108
|
/**
|
|
79
109
|
* Get masked user info (email, phone, authenticator status)
|
|
@@ -87,16 +117,15 @@ class AccountApi {
|
|
|
87
117
|
// Authenticated request - proxy through Next API to IDP
|
|
88
118
|
// Empty body: user info comes from JWT token
|
|
89
119
|
const result = await standardized_client_api_1.standardizedApi.post('/api/account/masked-info', {}, accessToken);
|
|
120
|
+
// PAY-1427: masked email/phone are still user data - log the response shape, never its values.
|
|
90
121
|
console.log('[DEBUG] getMaskedInfo result:', {
|
|
91
122
|
success: (0, standardized_client_api_1.isApiSuccess)(result),
|
|
92
123
|
hasData: (0, standardized_client_api_1.isApiSuccess)(result) && !!result.data,
|
|
93
|
-
rawData: (0, standardized_client_api_1.isApiSuccess)(result) ? result.data : null,
|
|
94
124
|
dataFields: (0, standardized_client_api_1.isApiSuccess)(result) && result.data ? Object.keys(result.data) : []
|
|
95
125
|
});
|
|
96
|
-
console.log('[DEBUG] getMaskedInfo raw data structure:', JSON.stringify((0, standardized_client_api_1.isApiSuccess)(result) ? result.data : result, null, 2));
|
|
97
126
|
if (!(0, standardized_client_api_1.isApiSuccess)(result)) {
|
|
98
127
|
const errorMessage = (0, standardized_client_api_1.isApiError)(result) ? result.message : 'Failed to get masked info';
|
|
99
|
-
console.error('[DEBUG] getMaskedInfo failed:', result);
|
|
128
|
+
console.error('[DEBUG] getMaskedInfo failed:', { keys: result && typeof result === 'object' ? Object.keys(result) : typeof result });
|
|
100
129
|
throw new ApiError(errorMessage, result);
|
|
101
130
|
}
|
|
102
131
|
// Use only snake_case fields from backend response
|
|
@@ -116,6 +145,9 @@ class AccountApi {
|
|
|
116
145
|
headers: { 'Content-Type': 'application/json' },
|
|
117
146
|
body: JSON.stringify({ email }),
|
|
118
147
|
});
|
|
148
|
+
// PAY-1588: a throttled caller (429) must look exactly like an unknown one - no session, no distinguishing error.
|
|
149
|
+
if (res.status === 429)
|
|
150
|
+
return { success: false, data: {} };
|
|
119
151
|
if (!res.ok)
|
|
120
152
|
throw new Error('Failed to initiate recovery');
|
|
121
153
|
return res.json();
|
|
@@ -154,13 +186,23 @@ class AccountApi {
|
|
|
154
186
|
headers: { 'Content-Type': 'application/json' },
|
|
155
187
|
body: JSON.stringify({
|
|
156
188
|
email,
|
|
157
|
-
|
|
189
|
+
token: resetToken,
|
|
158
190
|
new_password: newPassword,
|
|
159
191
|
confirm_password: confirmPassword,
|
|
160
192
|
}),
|
|
161
193
|
});
|
|
162
|
-
if (!res.ok)
|
|
163
|
-
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
const body = await res.json().catch(() => null);
|
|
196
|
+
const policy = !!body?.password_policy;
|
|
197
|
+
const rules = (0, password_rules_1.knownPasswordRules)(body?.password_policy?.rules);
|
|
198
|
+
const tokenInvalid = body?.reset_token_invalid === true;
|
|
199
|
+
const err = new Error(resetFailureText(res.status, rules, policy, tokenInvalid));
|
|
200
|
+
if (rules.length)
|
|
201
|
+
err.rules = rules;
|
|
202
|
+
if ((res.status === 400 || res.status === 422) && tokenInvalid && !policy)
|
|
203
|
+
err.code = 'RECOVERY_EXPIRED';
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
164
206
|
return res.json();
|
|
165
207
|
}
|
|
166
208
|
async resetPassword(data) {
|
|
@@ -171,8 +213,8 @@ class AccountApi {
|
|
|
171
213
|
body: JSON.stringify({
|
|
172
214
|
email: data.email,
|
|
173
215
|
token: data.token,
|
|
174
|
-
|
|
175
|
-
|
|
216
|
+
new_password: data.password,
|
|
217
|
+
confirm_password: data.password_confirmation
|
|
176
218
|
})
|
|
177
219
|
});
|
|
178
220
|
const result = await res.json().catch(() => ({}));
|
package/dist/vibe/sessions.js
CHANGED
|
@@ -105,7 +105,8 @@ async function vibeProxyRequest(endpoint, method, accessToken, body) {
|
|
|
105
105
|
}
|
|
106
106
|
if (!response.ok) {
|
|
107
107
|
const errorText = await response.text();
|
|
108
|
-
|
|
108
|
+
// PAY-1427: never the raw proxy body - the request carried a Bearer token and an error page can echo it.
|
|
109
|
+
console.error(`[vibe-sessions] Proxy error ${response.status}:`, { bodyLength: errorText.length });
|
|
109
110
|
return null;
|
|
110
111
|
}
|
|
111
112
|
const data = await response.json();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payez/next-mvp",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.6",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"dev:local": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/dev-local.ps1",
|
|
25
25
|
"dev:broker": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/dev-broker.ps1",
|
|
26
26
|
"check:publish-health": "node scripts/check-publish-health.mjs",
|
|
27
|
+
"check:provenance": "node scripts/verify-release-provenance.mjs",
|
|
27
28
|
"census:reach": "node scripts/module-reach.mjs",
|
|
28
29
|
"build:incremental": "npx tsc --project tsconfig.json"
|
|
29
30
|
},
|