@payez/next-mvp 4.4.9 → 4.5.1
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 +53 -0
- package/dist/api-handlers/onboarding/send-email-code.d.ts +11 -0
- package/dist/api-handlers/onboarding/send-email-code.js +11 -0
- package/dist/api-handlers/onboarding/set-password.d.ts +12 -0
- package/dist/api-handlers/onboarding/set-password.js +15 -0
- package/dist/api-handlers/onboarding/verify-code.d.ts +11 -0
- package/dist/api-handlers/onboarding/verify-code.js +14 -0
- package/dist/api-handlers/onboarding/verify-welcome-email.d.ts +10 -0
- package/dist/api-handlers/onboarding/verify-welcome-email.js +11 -0
- package/dist/auth/auth-decision.d.ts +5 -1
- package/dist/auth/auth-decision.js +11 -1
- package/dist/auth/password-reset-bridge.d.ts +70 -0
- package/dist/auth/password-reset-bridge.js +99 -0
- package/dist/auth/route-config.js +17 -0
- package/dist/components/auth/PasswordResetBridge.d.ts +17 -0
- package/dist/components/auth/PasswordResetBridge.js +32 -0
- package/dist/edge/middleware.d.ts +1 -1
- package/dist/edge/middleware.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/lib/onboarding-proxy.d.ts +30 -0
- package/dist/lib/onboarding-proxy.js +62 -0
- package/dist/lib/session-store.d.ts +17 -0
- package/dist/lib/session-store.js +33 -0
- package/dist/middleware/create-middleware.d.ts +10 -0
- package/dist/middleware/create-middleware.js +30 -1
- package/dist/pages/login/page.js +1 -1
- package/dist/pages/onboarding/index.d.ts +11 -0
- package/dist/pages/onboarding/index.js +20 -0
- package/dist/pages/onboarding/page.d.ts +1 -0
- package/dist/pages/onboarding/page.js +118 -0
- package/dist/pages/recovery/page.js +20 -7
- package/dist/routes/onboarding/index.d.ts +15 -0
- package/dist/routes/onboarding/index.js +22 -0
- package/dist/routes/onboarding/send-email-code.d.ts +2 -0
- package/dist/routes/onboarding/send-email-code.js +6 -0
- package/dist/routes/onboarding/set-password.d.ts +2 -0
- package/dist/routes/onboarding/set-password.js +6 -0
- package/dist/routes/onboarding/verify-code.d.ts +2 -0
- package/dist/routes/onboarding/verify-code.js +6 -0
- package/dist/routes/onboarding/verify-welcome-email.d.ts +2 -0
- package/dist/routes/onboarding/verify-welcome-email.js +6 -0
- package/dist/server/auth.js +87 -3
- package/dist/vibe/sessions.d.ts +10 -22
- package/dist/vibe/sessions.js +159 -24
- package/package.json +56 -1
- package/src/api-handlers/onboarding/send-email-code.ts +18 -0
- package/src/api-handlers/onboarding/set-password.ts +21 -0
- package/src/api-handlers/onboarding/verify-code.ts +20 -0
- package/src/api-handlers/onboarding/verify-welcome-email.ts +17 -0
- package/src/auth/auth-decision.ts +14 -1
- package/src/auth/password-reset-bridge.ts +98 -0
- package/src/auth/route-config.ts +17 -0
- package/src/components/auth/PasswordResetBridge.tsx +30 -0
- package/src/edge/middleware.ts +1 -1
- package/src/index.ts +1 -1
- package/src/lib/onboarding-proxy.ts +57 -0
- package/src/lib/session-store.ts +42 -0
- package/src/middleware/create-middleware.ts +26 -1
- package/src/pages/login/page.tsx +4 -7
- package/src/pages/onboarding/index.ts +11 -0
- package/src/pages/onboarding/page.tsx +212 -0
- package/src/pages/recovery/page.tsx +24 -8
- package/src/routes/onboarding/index.ts +15 -0
- package/src/routes/onboarding/send-email-code.ts +2 -0
- package/src/routes/onboarding/set-password.ts +2 -0
- package/src/routes/onboarding/verify-code.ts +2 -0
- package/src/routes/onboarding/verify-welcome-email.ts +2 -0
- package/src/server/auth.ts +90 -3
- package/src/vibe/document-unwrap.ts +103 -103
- package/src/vibe/sessions.ts +172 -27
|
@@ -37,6 +37,7 @@ exports.releaseRefreshLock = releaseRefreshLock;
|
|
|
37
37
|
exports.checkRefreshLock = checkRefreshLock;
|
|
38
38
|
exports.isRefreshInProgress = isRefreshInProgress;
|
|
39
39
|
exports.cleanupRefreshLocks = cleanupRefreshLocks;
|
|
40
|
+
exports.describeSessionKeyPresence = describeSessionKeyPresence;
|
|
40
41
|
const redis_1 = __importDefault(require("./redis"));
|
|
41
42
|
const crypto_1 = require("crypto");
|
|
42
43
|
const app_slug_1 = require("./app-slug");
|
|
@@ -927,3 +928,35 @@ async function cleanupRefreshLocks() {
|
|
|
927
928
|
console.warn('[SESSION-STORE] cleanupRefreshLocks called - scanning for expired locks');
|
|
928
929
|
return 0;
|
|
929
930
|
}
|
|
931
|
+
async function describeSessionKeyPresence(sessionToken) {
|
|
932
|
+
const ask = async (key) => {
|
|
933
|
+
try {
|
|
934
|
+
return (await redis_1.default.exists(key)) > 0;
|
|
935
|
+
}
|
|
936
|
+
catch {
|
|
937
|
+
return 'error';
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
let prefix = '';
|
|
941
|
+
try {
|
|
942
|
+
prefix = (0, app_slug_1.getSessionPrefix)();
|
|
943
|
+
}
|
|
944
|
+
catch {
|
|
945
|
+
prefix = '';
|
|
946
|
+
}
|
|
947
|
+
if (!sessionToken)
|
|
948
|
+
return { prefix, canonical: false, ba: false, ver: false };
|
|
949
|
+
let canonicalKey = '';
|
|
950
|
+
let baKey = '';
|
|
951
|
+
let verKey = '';
|
|
952
|
+
try {
|
|
953
|
+
canonicalKey = getSessionKey(sessionToken);
|
|
954
|
+
baKey = getBetterAuthSessionKey(sessionToken, (0, app_slug_1.getAppSlug)());
|
|
955
|
+
verKey = getSessionVersionKey(sessionToken);
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
return { prefix, canonical: 'error', ba: 'error', ver: 'error' };
|
|
959
|
+
}
|
|
960
|
+
const [canonical, ba, ver] = await Promise.all([ask(canonicalKey), ask(baKey), ask(verKey)]);
|
|
961
|
+
return { prefix, canonical, ba, ver };
|
|
962
|
+
}
|
|
@@ -124,6 +124,16 @@ export interface MvpMiddlewareOptions {
|
|
|
124
124
|
*/
|
|
125
125
|
revocationFailMode?: 'closed' | 'open';
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* PAY-2136 Rule A: a speculative request (router prefetch) must NEVER mutate cookies.
|
|
129
|
+
* A prefetch of /account/security was answered 307 + Set-Cookie (session cookies expired), the browser
|
|
130
|
+
* applied it, and the user's next real request (e.g. Approve) was signed out. Detected by any of:
|
|
131
|
+
* `Next-Router-Prefetch: 1|2` (Next sets it on EVERY Link prefetch; 2 = runtime prefetch) or `Sec-Purpose`/`Purpose`
|
|
132
|
+
* containing "prefetch".
|
|
133
|
+
* NOT `RSC: 1` + `_rsc`: a real client-side navigation carries both too (Nextpert-Scout 70976), so that shape
|
|
134
|
+
* would swallow in-app navigations and stop the middleware refreshing/clearing for them.
|
|
135
|
+
*/
|
|
136
|
+
export declare function isPrefetchRequest(request: NextRequest): boolean;
|
|
127
137
|
/** Login error code for a pre-2FA session that expired before the code was verified (PAY-884). */
|
|
128
138
|
export declare const TWO_FACTOR_TIMEOUT_ERROR = "TwoFactorTimeout";
|
|
129
139
|
export declare function createMvpMiddleware(options?: MvpMiddlewareOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
21
|
exports.TWO_FACTOR_TIMEOUT_ERROR = void 0;
|
|
22
|
+
exports.isPrefetchRequest = isPrefetchRequest;
|
|
22
23
|
exports.createMvpMiddleware = createMvpMiddleware;
|
|
23
24
|
exports.decideForceInvalid = decideForceInvalid;
|
|
24
25
|
const server_1 = require("next/server");
|
|
@@ -78,8 +79,32 @@ function isLoginPage(pathname) {
|
|
|
78
79
|
function getSafeCallbackUrl(pathname, searchParams) {
|
|
79
80
|
return (0, safe_callback_1.resolveCallbackDestination)(pathname, searchParams);
|
|
80
81
|
}
|
|
81
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* PAY-2136 Rule A: a speculative request (router prefetch) must NEVER mutate cookies.
|
|
84
|
+
* A prefetch of /account/security was answered 307 + Set-Cookie (session cookies expired), the browser
|
|
85
|
+
* applied it, and the user's next real request (e.g. Approve) was signed out. Detected by any of:
|
|
86
|
+
* `Next-Router-Prefetch: 1|2` (Next sets it on EVERY Link prefetch; 2 = runtime prefetch) or `Sec-Purpose`/`Purpose`
|
|
87
|
+
* containing "prefetch".
|
|
88
|
+
* NOT `RSC: 1` + `_rsc`: a real client-side navigation carries both too (Nextpert-Scout 70976), so that shape
|
|
89
|
+
* would swallow in-app navigations and stop the middleware refreshing/clearing for them.
|
|
90
|
+
*/
|
|
91
|
+
function isPrefetchRequest(request) {
|
|
92
|
+
const h = request.headers;
|
|
93
|
+
const nrp = h.get('next-router-prefetch');
|
|
94
|
+
if (nrp === '1' || nrp === '2')
|
|
95
|
+
return true;
|
|
96
|
+
if (/prefetch/i.test(h.get('sec-purpose') ?? '') || /prefetch/i.test(h.get('purpose') ?? ''))
|
|
97
|
+
return true;
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Clear all session cookies on a response. The ONE site that writes the expiry Set-Cookie for the middleware,
|
|
102
|
+
* so the prefetch guard lives here: on a prefetch the redirect is still returned (routing unchanged) but
|
|
103
|
+
* carries no Set-Cookie; the next real navigation reaches the same decision and clears then.
|
|
104
|
+
*/
|
|
82
105
|
function clearSessionCookies(response, request) {
|
|
106
|
+
if (isPrefetchRequest(request))
|
|
107
|
+
return;
|
|
83
108
|
const sessionCookie = (0, app_slug_1.getSessionCookieName)();
|
|
84
109
|
const secureCookie = (0, app_slug_1.getSecureSessionCookieName)();
|
|
85
110
|
// Clear main cookies
|
|
@@ -546,6 +571,10 @@ async function executeDecision(request, decision, pathname, sessionPointer, sess
|
|
|
546
571
|
case 'service_error':
|
|
547
572
|
return server_1.NextResponse.redirect(new URL('/service-unavailable', request.url));
|
|
548
573
|
case 'refresh_needed':
|
|
574
|
+
// PAY-2136: a speculative request must not spend (rotate) a single-use refresh token or, on a 401/403,
|
|
575
|
+
// clear the session. Let it through; the real navigation refreshes.
|
|
576
|
+
if (isPrefetchRequest(request))
|
|
577
|
+
return server_1.NextResponse.next();
|
|
549
578
|
return handleRefresh(request, safeCallback, opts);
|
|
550
579
|
}
|
|
551
580
|
}
|
package/dist/pages/login/page.js
CHANGED
|
@@ -206,7 +206,7 @@ function LoginForm({ altAuthSlot }) {
|
|
|
206
206
|
.themed-status-submitting svg {
|
|
207
207
|
color: ${buttonColor} !important;
|
|
208
208
|
}
|
|
209
|
-
` } }), (0, jsx_runtime_1.jsx)("div", { className: `min-h-screen flex items-center justify-center px-4 ${colors.background}`, children: (0, jsx_runtime_1.
|
|
209
|
+
` } }), (0, jsx_runtime_1.jsx)("div", { className: `min-h-screen flex items-center justify-center px-4 ${colors.background}`, children: (0, jsx_runtime_1.jsx)("div", { className: "max-w-md w-full", children: (0, jsx_runtime_1.jsxs)("div", { className: `rounded-2xl shadow-xl p-8 ${colors.card} ${colors.border} text-gray-800`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "text-center mb-8", children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-3xl font-bold mb-2", style: { color: 'inherit' }, children: branding.tagline || 'Welcome Back' }), (0, jsx_runtime_1.jsx)("p", { className: colors.muted, children: "Sign in to your account" })] }), googleEnabled && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => void handleGoogleSignIn(), disabled: googleLoading || isSubmitting, "data-testid": "google-sign-in", className: "w-full px-4 py-3 mb-6 rounded-lg border border-gray-300 font-semibold transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50", style: { backgroundColor: 'white', color: '#1f2937' }, children: [googleLoading ? ((0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin w-5 h-5", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" })] })) : ((0, jsx_runtime_1.jsxs)("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", children: [(0, jsx_runtime_1.jsx)("path", { d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z", fill: "#4285F4" }), (0, jsx_runtime_1.jsx)("path", { d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z", fill: "#34A853" }), (0, jsx_runtime_1.jsx)("path", { d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z", fill: "#FBBC05" }), (0, jsx_runtime_1.jsx)("path", { d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z", fill: "#EA4335" })] })), googleLoading ? 'Redirecting to Google…' : 'Continue with Google'] }) })), altAuthSlot && (0, jsx_runtime_1.jsx)("div", { className: "mt-4", children: altAuthSlot }), (0, jsx_runtime_1.jsx)("div", { className: `mt-6 text-center text-sm ${colors.muted}`, children: (0, jsx_runtime_1.jsx)("a", { href: "/account-auth/recovery", className: "themed-link hover:underline", children: "Start account recovery" }) })] }) }) })] }));
|
|
210
210
|
}
|
|
211
211
|
function LoginPageFallback() {
|
|
212
212
|
const colors = (0, useTheme_1.useColors)();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAY-2121: the onboarding surface.
|
|
3
|
+
*
|
|
4
|
+
* default - the /account-auth/onboarding page (mount from the scaffold at
|
|
5
|
+
* app/account-auth/onboarding/page.tsx).
|
|
6
|
+
* PasswordResetBridge - mount once on `/` to honour the IdP's post-verify
|
|
7
|
+
* `#password_reset_required=true` fragment.
|
|
8
|
+
*/
|
|
9
|
+
export { default } from './page';
|
|
10
|
+
export { default as OnboardingPage } from './page';
|
|
11
|
+
export { PasswordResetBridge } from '../../components/auth/PasswordResetBridge';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PasswordResetBridge = exports.OnboardingPage = exports.default = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* PAY-2121: the onboarding surface.
|
|
9
|
+
*
|
|
10
|
+
* default - the /account-auth/onboarding page (mount from the scaffold at
|
|
11
|
+
* app/account-auth/onboarding/page.tsx).
|
|
12
|
+
* PasswordResetBridge - mount once on `/` to honour the IdP's post-verify
|
|
13
|
+
* `#password_reset_required=true` fragment.
|
|
14
|
+
*/
|
|
15
|
+
var page_1 = require("./page");
|
|
16
|
+
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(page_1).default; } });
|
|
17
|
+
var page_2 = require("./page");
|
|
18
|
+
Object.defineProperty(exports, "OnboardingPage", { enumerable: true, get: function () { return __importDefault(page_2).default; } });
|
|
19
|
+
var PasswordResetBridge_1 = require("../../components/auth/PasswordResetBridge");
|
|
20
|
+
Object.defineProperty(exports, "PasswordResetBridge", { enumerable: true, get: function () { return PasswordResetBridge_1.PasswordResetBridge; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function OnboardingPage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.default = OnboardingPage;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const navigation_1 = require("next/navigation");
|
|
8
|
+
const H = { primary: 'var(--text-primary)', secondary: 'var(--text-secondary)', muted: 'var(--text-muted)' };
|
|
9
|
+
function OnboardingContent() {
|
|
10
|
+
const params = (0, navigation_1.useSearchParams)();
|
|
11
|
+
const onboardToken = params?.get('onboard_token') ?? '';
|
|
12
|
+
const [step, setStep] = (0, react_1.useState)('code');
|
|
13
|
+
const [maskedEmail, setMaskedEmail] = (0, react_1.useState)(null);
|
|
14
|
+
const [digits, setDigits] = (0, react_1.useState)('');
|
|
15
|
+
const [password, setPassword] = (0, react_1.useState)('');
|
|
16
|
+
const [confirm, setConfirm] = (0, react_1.useState)('');
|
|
17
|
+
const [resetToken, setResetToken] = (0, react_1.useState)('');
|
|
18
|
+
const [email, setEmail] = (0, react_1.useState)('');
|
|
19
|
+
const [busy, setBusy] = (0, react_1.useState)(false);
|
|
20
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
21
|
+
// Step 1: tell the invitee which address the code went to (masked). Best
|
|
22
|
+
// effort - a failure here does not block entering a code they were emailed.
|
|
23
|
+
(0, react_1.useEffect)(() => {
|
|
24
|
+
if (!onboardToken)
|
|
25
|
+
return;
|
|
26
|
+
let alive = true;
|
|
27
|
+
(async () => {
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch('/api/onboarding/verify-welcome-email', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({ onboard_token: onboardToken }),
|
|
33
|
+
});
|
|
34
|
+
const body = await res.json().catch(() => null);
|
|
35
|
+
const data = body?.data?.data || body?.data || {};
|
|
36
|
+
const masked = data?.masked_user_info?.masked_email || data?.masked_email;
|
|
37
|
+
if (alive && typeof masked === 'string' && masked)
|
|
38
|
+
setMaskedEmail(masked);
|
|
39
|
+
}
|
|
40
|
+
catch { /* non-fatal */ }
|
|
41
|
+
})();
|
|
42
|
+
return () => { alive = false; };
|
|
43
|
+
}, [onboardToken]);
|
|
44
|
+
if (!onboardToken) {
|
|
45
|
+
return ((0, jsx_runtime_1.jsxs)(Shell, { children: [(0, jsx_runtime_1.jsx)("h1", { style: titleStyle, children: "This invitation link is missing its token." }), (0, jsx_runtime_1.jsx)("p", { style: { color: H.secondary, fontSize: 14, marginTop: 8 }, children: "The link may have been truncated or copied incompletely. Ask for a new invitation, or sign in if you already have an account." }), (0, jsx_runtime_1.jsx)("p", { style: { marginTop: 16 }, children: (0, jsx_runtime_1.jsx)("a", { href: "/account-auth/login", style: linkStyle, children: "Go to sign in" }) })] }));
|
|
46
|
+
}
|
|
47
|
+
async function submitCode(e) {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
setBusy(true);
|
|
50
|
+
setError(null);
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch('/api/onboarding/verify-code', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({ onboard_token: onboardToken, code: digits }),
|
|
56
|
+
});
|
|
57
|
+
const body = await res.json().catch(() => null);
|
|
58
|
+
const data = body?.data?.data || body?.data || body;
|
|
59
|
+
if (!res.ok || !data?.reset_token || !data?.email)
|
|
60
|
+
throw new Error();
|
|
61
|
+
setResetToken(data.reset_token);
|
|
62
|
+
setEmail(data.email);
|
|
63
|
+
setStep('password');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
setError('That code was not accepted. Check it and try again.');
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
setBusy(false);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function submitPassword(e) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
if (password !== confirm) {
|
|
75
|
+
setError('The two passwords do not match.');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
setBusy(true);
|
|
79
|
+
setError(null);
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch('/api/onboarding/set-password', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({ onboard_token: onboardToken, email, reset_token: resetToken, new_password: password }),
|
|
85
|
+
});
|
|
86
|
+
const body = await res.json().catch(() => null);
|
|
87
|
+
if (!res.ok || (body && body.success === false))
|
|
88
|
+
throw new Error(body?.message || '');
|
|
89
|
+
setStep('done');
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
setError(err instanceof Error && err.message ? err.message : 'We could not set that password. Try again.');
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
setBusy(false);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return ((0, jsx_runtime_1.jsxs)(Shell, { children: [step === 'code' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h1", { style: titleStyle, children: "Finish setting up your account" }), (0, jsx_runtime_1.jsx)("p", { style: { color: H.secondary, fontSize: 14, marginTop: 8 }, children: maskedEmail
|
|
99
|
+
? `We emailed a 6-digit code to ${maskedEmail}. Enter it below.`
|
|
100
|
+
: 'Enter the 6-digit code from your invitation email.' }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: submitCode, noValidate: true, style: { marginTop: 16 }, children: [(0, jsx_runtime_1.jsx)("input", { value: digits, onChange: (e) => setDigits(e.target.value.replace(/\D/g, '').slice(0, 6)), inputMode: "numeric", autoComplete: "one-time-code", "aria-label": "6-digit code", placeholder: "000000", style: { ...inputStyle, letterSpacing: 6, textAlign: 'center', fontFamily: 'monospace', fontSize: 20 } }), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: busy || digits.length < 6, style: ctaStyle(busy || digits.length < 6), children: busy ? 'Verifying…' : 'Verify' })] })] })), step === 'password' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h1", { style: titleStyle, children: "Choose a password" }), (0, jsx_runtime_1.jsx)("p", { style: { color: H.secondary, fontSize: 14, marginTop: 8 }, children: "Set the password you will sign in with." }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: submitPassword, noValidate: true, style: { marginTop: 16 }, children: [(0, jsx_runtime_1.jsx)("input", { type: "password", value: password, onChange: (e) => setPassword(e.target.value), "aria-label": "New password", placeholder: "New password", style: inputStyle }), (0, jsx_runtime_1.jsx)("input", { type: "password", value: confirm, onChange: (e) => setConfirm(e.target.value), "aria-label": "Confirm password", placeholder: "Confirm password", style: { ...inputStyle, marginTop: 10 } }), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: busy || !password || !confirm, style: ctaStyle(busy || !password || !confirm), children: busy ? 'Saving…' : 'Set password' })] })] })), step === 'done' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h1", { style: titleStyle, children: "You're all set." }), (0, jsx_runtime_1.jsx)("p", { style: { color: H.secondary, fontSize: 14, marginTop: 8 }, children: "Your account is ready. You can sign in now." }), (0, jsx_runtime_1.jsx)("p", { style: { marginTop: 16 }, children: (0, jsx_runtime_1.jsx)("a", { href: "/account-auth/login", style: linkStyle, children: "Go to sign in" }) })] })), error && (0, jsx_runtime_1.jsx)("p", { style: { color: '#b00020', fontSize: 13, marginTop: 12 }, children: error })] }));
|
|
101
|
+
}
|
|
102
|
+
const titleStyle = { color: H.primary, fontSize: 20, fontWeight: 600, margin: 0 };
|
|
103
|
+
const linkStyle = { color: H.primary, fontWeight: 600, textDecoration: 'underline' };
|
|
104
|
+
const inputStyle = {
|
|
105
|
+
width: '100%', padding: '10px 12px', border: '1px solid var(--border-default, #d1d5db)',
|
|
106
|
+
borderRadius: 10, color: H.primary, fontSize: 15, background: 'var(--bg-card, #fff)',
|
|
107
|
+
};
|
|
108
|
+
const ctaStyle = (disabled) => ({
|
|
109
|
+
marginTop: 14, width: '100%', padding: '10px 12px', borderRadius: 10, border: 'none',
|
|
110
|
+
background: H.primary, color: '#fff', fontWeight: 600, fontSize: 15, cursor: disabled ? 'default' : 'pointer',
|
|
111
|
+
opacity: disabled ? 0.5 : 1,
|
|
112
|
+
});
|
|
113
|
+
function Shell({ children }) {
|
|
114
|
+
return ((0, jsx_runtime_1.jsx)("div", { style: { minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16, background: 'var(--bg-default, #f9fafb)' }, children: (0, jsx_runtime_1.jsx)("div", { style: { width: '100%', maxWidth: 420, background: 'var(--bg-card, #fff)', border: '1px solid var(--border-default, #e5e7eb)', borderRadius: 16, padding: 28 }, children: children }) }));
|
|
115
|
+
}
|
|
116
|
+
function OnboardingPage() {
|
|
117
|
+
return ((0, jsx_runtime_1.jsx)(react_1.Suspense, { fallback: (0, jsx_runtime_1.jsx)("div", { style: { minHeight: '100vh' } }), children: (0, jsx_runtime_1.jsx)(OnboardingContent, {}) }));
|
|
118
|
+
}
|
|
@@ -6,6 +6,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
6
6
|
const react_1 = require("react");
|
|
7
7
|
const navigation_1 = require("next/navigation");
|
|
8
8
|
const api_1 = require("../../utils/api");
|
|
9
|
+
const password_reset_bridge_1 = require("../../auth/password-reset-bridge");
|
|
9
10
|
const InitiateRecoveryStep_1 = require("../../components/recovery/InitiateRecoveryStep");
|
|
10
11
|
const SelectMethodStep_1 = require("../../components/recovery/SelectMethodStep");
|
|
11
12
|
const VerifyCodeStep_1 = require("../../components/recovery/VerifyCodeStep");
|
|
@@ -16,14 +17,18 @@ const useTheme_1 = require("../../theme/useTheme");
|
|
|
16
17
|
function RecoveryContent() {
|
|
17
18
|
const router = (0, navigation_1.useRouter)();
|
|
18
19
|
const searchParams = (0, navigation_1.useSearchParams)();
|
|
19
|
-
|
|
20
|
+
// PAY-2121 (BAPert 70576): a VERIFIED passwordless user arrives here with a real
|
|
21
|
+
// reset token (?token=) from the IdP's post-verify fragment, via the bridge. In
|
|
22
|
+
// that case skip the request-a-code step and go straight to set-password - the
|
|
23
|
+
// token is the credential; asking for a code would throw it away.
|
|
24
|
+
const entry = (0, password_reset_bridge_1.recoveryEntry)(searchParams?.get('token'), searchParams?.get('email'));
|
|
20
25
|
const colors = (0, useTheme_1.useColors)();
|
|
21
|
-
const [currentStep, setCurrentStep] = (0, react_1.useState)(
|
|
22
|
-
const [email, setEmail] = (0, react_1.useState)(
|
|
26
|
+
const [currentStep, setCurrentStep] = (0, react_1.useState)(entry.step);
|
|
27
|
+
const [email, setEmail] = (0, react_1.useState)(entry.email);
|
|
23
28
|
const [recoverySession, setRecoverySession] = (0, react_1.useState)(null);
|
|
24
29
|
const [selectedMethod, setSelectedMethod] = (0, react_1.useState)(null);
|
|
25
30
|
const [verificationCode, setVerificationCode] = (0, react_1.useState)('');
|
|
26
|
-
const [passwordResetToken, setPasswordResetToken] = (0, react_1.useState)(null);
|
|
31
|
+
const [passwordResetToken, setPasswordResetToken] = (0, react_1.useState)(entry.resetToken ? { token: entry.resetToken, expiresAt: '' } : null);
|
|
27
32
|
const [error, setError] = (0, react_1.useState)(null);
|
|
28
33
|
// PAY-1591: true when the reset failed because the recovery itself is dead; the error box then offers a way back.
|
|
29
34
|
const [recoveryExpired, setRecoveryExpired] = (0, react_1.useState)(false);
|
|
@@ -108,13 +113,19 @@ function RecoveryContent() {
|
|
|
108
113
|
}
|
|
109
114
|
};
|
|
110
115
|
const handleResetPassword = async (password, confirmPassword) => {
|
|
111
|
-
|
|
116
|
+
// PAY-2121 (BAPert 70582): the ONE place that decides whether a submit may proceed.
|
|
117
|
+
// No token (or no email anywhere) -> refuse BEFORE any request; a guard-less submit
|
|
118
|
+
// would otherwise call the API with an empty token.
|
|
119
|
+
const target = (0, password_reset_bridge_1.resetSubmitTarget)(passwordResetToken?.token, recoverySession?.email, email);
|
|
120
|
+
if (!target) {
|
|
121
|
+
setError(passwordResetToken ? 'We could not tell which account to reset. Start recovery again.' : 'This reset link is missing its token. Start recovery again.');
|
|
112
122
|
return;
|
|
123
|
+
}
|
|
113
124
|
setLoading(true);
|
|
114
125
|
setError(null);
|
|
115
126
|
setRecoveryExpired(false);
|
|
116
127
|
try {
|
|
117
|
-
const response = await api_1.accountApi.resetPasswordWithToken(
|
|
128
|
+
const response = await api_1.accountApi.resetPasswordWithToken(target.email, target.token, password, confirmPassword);
|
|
118
129
|
if (response.success) {
|
|
119
130
|
setResetSucceeded(true);
|
|
120
131
|
setCurrentStep('complete');
|
|
@@ -136,8 +147,10 @@ function RecoveryContent() {
|
|
|
136
147
|
setCurrentStep('initiate');
|
|
137
148
|
else if (currentStep === 'verify-code')
|
|
138
149
|
setCurrentStep('select-method');
|
|
150
|
+
// PAY-2121: a deep-linked set-password has no verify-code to go back to; return to
|
|
151
|
+
// the request-code step so a bad token still leaves a way forward.
|
|
139
152
|
else if (currentStep === 'set-password')
|
|
140
|
-
setCurrentStep('verify-code');
|
|
153
|
+
setCurrentStep(recoverySession ? 'verify-code' : 'initiate');
|
|
141
154
|
};
|
|
142
155
|
return ((0, jsx_runtime_1.jsx)("div", { className: "w-full flex items-center justify-center p-6", style: { background: 'var(--bg-default)' }, children: (0, jsx_runtime_1.jsxs)("div", { className: "max-w-md w-full", children: [error && ((0, jsx_runtime_1.jsxs)("div", { className: "mb-4 bg-red-50 border border-red-200 rounded-2xl p-3", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-red-700 text-sm", children: error }), recoveryExpired && ((0, jsx_runtime_1.jsx)("a", { href: "/account-auth/recovery", className: "text-sm font-medium underline", children: "Start password recovery again" }))] })), loading && ((0, jsx_runtime_1.jsx)("div", { className: "mb-4 rounded-2xl p-3", style: { background: 'var(--bg-card)', borderColor: 'var(--border-default)' }, children: (0, jsx_runtime_1.jsx)("p", { className: "text-sm", style: { color: 'var(--text-secondary)' }, children: "Processing..." }) })), currentStep === 'initiate' && ((0, jsx_runtime_1.jsx)(InitiateRecoveryStep_1.InitiateRecoveryStep, { email: email, setEmail: setEmail, onSubmit: handleInitiateRecovery, loading: loading })), currentStep === 'select-method' && recoverySession && ((0, jsx_runtime_1.jsx)(SelectMethodStep_1.SelectMethodStep, { session: recoverySession, onSelectMethod: handleSendCode, loading: loading })), currentStep === 'verify-code' && ((0, jsx_runtime_1.jsx)(VerifyCodeStep_1.VerifyCodeStep, { code: verificationCode, setCode: setVerificationCode, onSubmit: handleVerifyCode, onResend: () => selectedMethod && handleSendCode(selectedMethod), loading: loading, maskedDestination: selectedMethod === 'sms'
|
|
143
156
|
? recoverySession?.maskedPhone
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ready-to-use onboarding route handlers (PAY-2121).
|
|
3
|
+
*
|
|
4
|
+
* Import directly into your app's API routes with zero configuration:
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // app/api/onboarding/verify-code/route.ts
|
|
9
|
+
* export { POST } from '@payez/next-mvp/routes/onboarding/verify-code';
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export { POST as verifyCodePOST } from '../../api-handlers/onboarding/verify-code';
|
|
13
|
+
export { POST as sendEmailCodePOST } from '../../api-handlers/onboarding/send-email-code';
|
|
14
|
+
export { POST as verifyWelcomeEmailPOST } from '../../api-handlers/onboarding/verify-welcome-email';
|
|
15
|
+
export { POST as setPasswordPOST } from '../../api-handlers/onboarding/set-password';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setPasswordPOST = exports.verifyWelcomeEmailPOST = exports.sendEmailCodePOST = exports.verifyCodePOST = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Ready-to-use onboarding route handlers (PAY-2121).
|
|
6
|
+
*
|
|
7
|
+
* Import directly into your app's API routes with zero configuration:
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* // app/api/onboarding/verify-code/route.ts
|
|
12
|
+
* export { POST } from '@payez/next-mvp/routes/onboarding/verify-code';
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
var verify_code_1 = require("../../api-handlers/onboarding/verify-code");
|
|
16
|
+
Object.defineProperty(exports, "verifyCodePOST", { enumerable: true, get: function () { return verify_code_1.POST; } });
|
|
17
|
+
var send_email_code_1 = require("../../api-handlers/onboarding/send-email-code");
|
|
18
|
+
Object.defineProperty(exports, "sendEmailCodePOST", { enumerable: true, get: function () { return send_email_code_1.POST; } });
|
|
19
|
+
var verify_welcome_email_1 = require("../../api-handlers/onboarding/verify-welcome-email");
|
|
20
|
+
Object.defineProperty(exports, "verifyWelcomeEmailPOST", { enumerable: true, get: function () { return verify_welcome_email_1.POST; } });
|
|
21
|
+
var set_password_1 = require("../../api-handlers/onboarding/set-password");
|
|
22
|
+
Object.defineProperty(exports, "setPasswordPOST", { enumerable: true, get: function () { return set_password_1.POST; } });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = void 0;
|
|
4
|
+
/** POST /api/onboarding/send-email-code -> IdP (PAY-2121). */
|
|
5
|
+
var send_email_code_1 = require("../../api-handlers/onboarding/send-email-code");
|
|
6
|
+
Object.defineProperty(exports, "POST", { enumerable: true, get: function () { return send_email_code_1.POST; } });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = void 0;
|
|
4
|
+
/** POST /api/onboarding/set-password -> IdP (PAY-2121). */
|
|
5
|
+
var set_password_1 = require("../../api-handlers/onboarding/set-password");
|
|
6
|
+
Object.defineProperty(exports, "POST", { enumerable: true, get: function () { return set_password_1.POST; } });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = void 0;
|
|
4
|
+
/** POST /api/onboarding/verify-code -> IdP (PAY-2121). */
|
|
5
|
+
var verify_code_1 = require("../../api-handlers/onboarding/verify-code");
|
|
6
|
+
Object.defineProperty(exports, "POST", { enumerable: true, get: function () { return verify_code_1.POST; } });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = void 0;
|
|
4
|
+
/** POST /api/onboarding/verify-welcome-email -> IdP (PAY-2121). */
|
|
5
|
+
var verify_welcome_email_1 = require("../../api-handlers/onboarding/verify-welcome-email");
|
|
6
|
+
Object.defineProperty(exports, "POST", { enumerable: true, get: function () { return verify_welcome_email_1.POST; } });
|
package/dist/server/auth.js
CHANGED
|
@@ -48,6 +48,7 @@ exports.requireSession = requireSession;
|
|
|
48
48
|
require("server-only");
|
|
49
49
|
const better_auth_1 = require("../auth/better-auth");
|
|
50
50
|
const session_store_1 = require("../lib/session-store");
|
|
51
|
+
const token_log_id_1 = require("../lib/token-log-id");
|
|
51
52
|
const verified_session_token_1 = require("./verified-session-token");
|
|
52
53
|
function buildSessionDataFromAuthSession(session) {
|
|
53
54
|
const user = session?.user;
|
|
@@ -138,6 +139,79 @@ function attachSessionData(session, sessionData, sessionToken) {
|
|
|
138
139
|
async function getAuthInstance() {
|
|
139
140
|
return (0, better_auth_1.getBetterAuthInstance)();
|
|
140
141
|
}
|
|
142
|
+
/* ----------------------------------------------------------------------------------------------------------------
|
|
143
|
+
* PAY-2136 DIAGNOSTICS (BAPert 70873, Nextpert-Scout 70872/70875) - LOG ONLY, NO BEHAVIOUR CHANGE.
|
|
144
|
+
*
|
|
145
|
+
* A getFreshIdpToken NO_SESSION used to carry no origin: three different returns produced the same
|
|
146
|
+
* `{error:'NO_SESSION'}`, and getSession() swallowed every Redis error into a bare `catch {}`. These helpers name
|
|
147
|
+
* the origin and make a swallowed read audible. Hard rules: every helper is wrapped so it can NEVER throw or change
|
|
148
|
+
* a return value; nothing here ever logs a token, a cookie VALUE or a Redis value - only a tokenLogId() fingerprint,
|
|
149
|
+
* cookie NAMES, booleans, and an error name/message with any long opaque run redacted.
|
|
150
|
+
* -------------------------------------------------------------------------------------------------------------- */
|
|
151
|
+
/** An error name/message, first 80 chars, with any 24+ char opaque run (a token, a key) replaced. */
|
|
152
|
+
function scrubForLog(v) {
|
|
153
|
+
const raw = v instanceof Error ? v.message : String(v ?? '');
|
|
154
|
+
return raw.replace(/[A-Za-z0-9_\-.+/=]{24,}/g, '[redacted]').slice(0, 80);
|
|
155
|
+
}
|
|
156
|
+
/** `catch {}` sites on the session read path: still swallowed, now audible. */
|
|
157
|
+
function logSessionReadSwallowed(path, err) {
|
|
158
|
+
try {
|
|
159
|
+
const name = scrubForLog(err instanceof Error ? err.name : typeof err);
|
|
160
|
+
console.warn(`[server-auth] SESSION_READ_SWALLOWED path=${path} err=${name} msg=${scrubForLog(err)}`);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
/* a diagnostic must never change the outcome */
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Origin a: getSession(request) produced no token. Cookie NAMES only, never a value. */
|
|
167
|
+
function logNoSessionOriginA(request) {
|
|
168
|
+
try {
|
|
169
|
+
const header = request?.headers?.get('cookie') ?? '';
|
|
170
|
+
// Only segments of the form name=value contribute a NAME. A segment with no '=' is not a cookie pair, and
|
|
171
|
+
// logging it whole could emit a bare token (Nextpert-Scout 70901 finding 1).
|
|
172
|
+
const cookieNames = header
|
|
173
|
+
.split(';')
|
|
174
|
+
// Structural, alphabet-independent (Nextpert-Scout 70945): base64 padding can only sit at the END of a bare
|
|
175
|
+
// value, so a bare padded token splits as name=<token>, value='' or '='. A real pair has a non-empty value that
|
|
176
|
+
// is not only '=' characters. (The old includes('=') let 74% of base64url tokens through as a "name".)
|
|
177
|
+
.filter((c) => {
|
|
178
|
+
const i = c.indexOf('=');
|
|
179
|
+
if (i < 0)
|
|
180
|
+
return false;
|
|
181
|
+
const v = c.slice(i + 1);
|
|
182
|
+
return v !== '' && !/^=+$/.test(v);
|
|
183
|
+
})
|
|
184
|
+
.map((c) => c.split('=')[0].trim())
|
|
185
|
+
.filter((n) => n !== '')
|
|
186
|
+
// A bare token that merely ENDS in base64 padding ('=') still passes the filter above and its "name" would be
|
|
187
|
+
// the token itself (Nextpert-Scout 70913). Real cookie names carry a separator ('.', '-', '_'); a long name with
|
|
188
|
+
// none is opaque, so it is logged as a placeholder, never as itself.
|
|
189
|
+
.map((n) => (n.length >= 24 && !/[._-]/.test(n) ? '[opaque-name]' : n))
|
|
190
|
+
.slice(0, 20);
|
|
191
|
+
console.warn('[server-auth] NO_SESSION origin=a', { hasCookieHeader: header !== '', cookieNames });
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
/* a diagnostic must never change the outcome */
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Origins b and c: a token WAS resolved. Log which of its three keys exist (presence only), fire-and-forget. */
|
|
198
|
+
function logNoSessionWithKeys(origin, sessionToken) {
|
|
199
|
+
try {
|
|
200
|
+
void (0, session_store_1.describeSessionKeyPresence)(sessionToken).then((keys) => {
|
|
201
|
+
try {
|
|
202
|
+
console.warn(`[server-auth] NO_SESSION origin=${origin}`, { token: (0, token_log_id_1.tokenLogId)(sessionToken), ...keys });
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* never throws */
|
|
206
|
+
}
|
|
207
|
+
}, () => {
|
|
208
|
+
/* never throws */
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
/* a diagnostic must never change the outcome */
|
|
213
|
+
}
|
|
214
|
+
}
|
|
141
215
|
/**
|
|
142
216
|
* Get the current session from a request.
|
|
143
217
|
*
|
|
@@ -167,7 +241,9 @@ async function getSession(request) {
|
|
|
167
241
|
try {
|
|
168
242
|
betterAuthSession = await auth.api.getSession({ headers: request.headers });
|
|
169
243
|
}
|
|
170
|
-
catch {
|
|
244
|
+
catch (err) {
|
|
245
|
+
logSessionReadSwallowed('primary', err); // fall through to canonical Redis path
|
|
246
|
+
}
|
|
171
247
|
if (betterAuthSession?.session?.token && betterAuthSession?.user) {
|
|
172
248
|
const sessionToken = betterAuthSession.session.token;
|
|
173
249
|
let sessionData = null;
|
|
@@ -176,7 +252,9 @@ async function getSession(request) {
|
|
|
176
252
|
if (!sessionData)
|
|
177
253
|
sessionData = await (0, session_store_1.getBetterAuthSession)(sessionToken);
|
|
178
254
|
}
|
|
179
|
-
catch {
|
|
255
|
+
catch (err) {
|
|
256
|
+
logSessionReadSwallowed('A', err); // Redis unavailable
|
|
257
|
+
}
|
|
180
258
|
if (!sessionData)
|
|
181
259
|
sessionData = buildSessionDataFromAuthSession(betterAuthSession);
|
|
182
260
|
return attachSessionData(betterAuthSession, sessionData, sessionToken);
|
|
@@ -201,7 +279,9 @@ async function getSession(request) {
|
|
|
201
279
|
if (!sessionData)
|
|
202
280
|
sessionData = await (0, session_store_1.getBetterAuthSession)(sessionToken);
|
|
203
281
|
}
|
|
204
|
-
catch {
|
|
282
|
+
catch (err) {
|
|
283
|
+
logSessionReadSwallowed('B', err); // Redis unavailable
|
|
284
|
+
}
|
|
205
285
|
if (!sessionData)
|
|
206
286
|
return null;
|
|
207
287
|
// Synthesize a minimal session shape compatible with downstream callers.
|
|
@@ -282,6 +362,7 @@ async function getFreshIdpToken(request, config) {
|
|
|
282
362
|
const session = await getSession(request);
|
|
283
363
|
const sessionToken = session?.session?.token;
|
|
284
364
|
if (!sessionToken) {
|
|
365
|
+
logNoSessionOriginA(request);
|
|
285
366
|
return { success: false, error: 'NO_SESSION', status: 401, terminal: true };
|
|
286
367
|
}
|
|
287
368
|
const { ensureFreshAccessToken } = await Promise.resolve().then(() => __importStar(require('../lib/ensure-fresh-access-token')));
|
|
@@ -294,6 +375,8 @@ async function getFreshIdpToken(request, config) {
|
|
|
294
375
|
requestId: request?.headers?.get('x-request-id') ?? undefined,
|
|
295
376
|
});
|
|
296
377
|
if (!result.ok) {
|
|
378
|
+
if (result.code === 'NO_SESSION')
|
|
379
|
+
logNoSessionWithKeys('b', sessionToken);
|
|
297
380
|
return {
|
|
298
381
|
success: false,
|
|
299
382
|
error: result.code,
|
|
@@ -306,6 +389,7 @@ async function getFreshIdpToken(request, config) {
|
|
|
306
389
|
}
|
|
307
390
|
const sessionData = await (0, session_store_1.getSession)(sessionToken);
|
|
308
391
|
if (!sessionData) {
|
|
392
|
+
logNoSessionWithKeys('c', sessionToken);
|
|
309
393
|
return { success: false, error: 'NO_SESSION', status: 401, terminal: true };
|
|
310
394
|
}
|
|
311
395
|
return {
|