@payez/next-mvp 4.4.5 → 4.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/dist/api/auth-handler.js +5 -3
- package/dist/api-handlers/account/change-password.js +33 -8
- package/dist/api-handlers/account/masked-info.js +2 -1
- package/dist/api-handlers/account/recovery/forward.d.ts +14 -0
- package/dist/api-handlers/account/recovery/forward.js +105 -0
- package/dist/api-handlers/account/recovery/initiate.js +4 -0
- package/dist/api-handlers/account/recovery/send-code.d.ts +12 -2
- package/dist/api-handlers/account/recovery/send-code.js +11 -84
- package/dist/api-handlers/account/recovery/verify-code.d.ts +11 -2
- package/dist/api-handlers/account/recovery/verify-code.js +10 -84
- package/dist/api-handlers/account/reset-password.js +24 -1
- package/dist/api-handlers/account/send-code.d.ts +1 -15
- package/dist/api-handlers/account/send-code.js +3 -6
- package/dist/api-handlers/account/update-phone.js +6 -1
- package/dist/api-handlers/account/validate-password.js +3 -1
- package/dist/api-handlers/account/verify-email.d.ts +1 -17
- package/dist/api-handlers/account/verify-email.js +4 -6
- package/dist/api-handlers/account/verify-sms.d.ts +1 -17
- package/dist/api-handlers/account/verify-sms.js +4 -6
- package/dist/api-handlers/admin/analytics.js +12 -4
- package/dist/api-handlers/admin/audit.js +12 -4
- package/dist/api-handlers/admin/redis-sessions.js +16 -8
- package/dist/api-handlers/admin/sessions.js +14 -6
- package/dist/api-handlers/admin/site-logs.js +39 -16
- package/dist/api-handlers/admin/stats.js +14 -4
- package/dist/api-handlers/admin/users.js +14 -6
- package/dist/api-handlers/admin/vibe-data.js +19 -11
- package/dist/api-handlers/auth/jwks.js +2 -1
- package/dist/api-handlers/auth/login.js +13 -4
- package/dist/api-handlers/auth/passkey-signin.js +6 -0
- package/dist/api-handlers/auth/refresh.js +38 -11
- package/dist/api-handlers/auth/signout.js +2 -1
- package/dist/api-handlers/auth/status.js +4 -1
- package/dist/api-handlers/auth/update-session.d.ts +25 -4
- package/dist/api-handlers/auth/update-session.js +38 -11
- package/dist/api-handlers/auth/validate.js +4 -2
- package/dist/api-handlers/auth/verify-code.js +3 -2
- package/dist/api-handlers/mfa/backup-code-verify.d.ts +1 -18
- package/dist/api-handlers/mfa/backup-code-verify.js +4 -6
- package/dist/api-handlers/mfa/backup-codes.js +2 -1
- package/dist/api-handlers/mfa/methods.js +2 -1
- package/dist/api-handlers/mfa/totp-confirm.js +2 -1
- package/dist/api-handlers/mfa/totp-enroll.js +2 -1
- package/dist/api-handlers/mfa/totp-verify.d.ts +1 -19
- package/dist/api-handlers/mfa/totp-verify.js +4 -6
- package/dist/api-handlers/passkeys/list.js +2 -1
- package/dist/api-handlers/passkeys/manage.js +2 -1
- package/dist/api-handlers/passkeys/register-options.js +2 -1
- package/dist/api-handlers/passkeys/register-verify.js +2 -1
- package/dist/api-handlers/session/refresh-viability.d.ts +4 -2
- package/dist/api-handlers/session/refresh-viability.js +10 -7
- package/dist/api-handlers/session/viability.js +4 -3
- package/dist/api-handlers/test/force-expire.js +5 -3
- package/dist/auth/auth-decision.js +2 -1
- package/dist/auth/better-auth.d.ts +58 -14
- package/dist/auth/better-auth.js +125 -54
- package/dist/auth/utils/idp-client.js +2 -1
- package/dist/client/better-auth-client.d.ts +101 -956
- package/dist/components/admin/DataBrowserTab.js +4 -1
- package/dist/components/admin/SessionsTab.js +9 -3
- package/dist/components/recovery/RequestedStep.d.ts +11 -0
- package/dist/components/recovery/RequestedStep.js +14 -0
- package/dist/components/recovery/SetPasswordStep.js +10 -1
- package/dist/config/env.d.ts +0 -2
- package/dist/config/env.js +0 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -1
- package/dist/lib/app-slug.d.ts +21 -0
- package/dist/lib/app-slug.js +51 -0
- package/dist/lib/ensure-fresh-access-token.d.ts +26 -0
- package/dist/lib/ensure-fresh-access-token.js +105 -4
- package/dist/lib/idp-client-config.d.ts +20 -0
- package/dist/lib/idp-client-config.js +73 -45
- package/dist/lib/idp-fetch.js +1 -1
- package/dist/lib/never-in-response.d.ts +30 -0
- package/dist/lib/never-in-response.js +71 -0
- package/dist/lib/password-rules.d.ts +22 -0
- package/dist/lib/password-rules.js +43 -0
- package/dist/lib/safe-error-response.d.ts +67 -0
- package/dist/lib/safe-error-response.js +130 -0
- package/dist/lib/session-cookie-shape.d.ts +22 -19
- package/dist/lib/session-cookie-shape.js +14 -18
- package/dist/lib/session-store.d.ts +8 -1
- package/dist/lib/session-store.js +170 -24
- package/dist/lib/startup-init.d.ts +6 -0
- package/dist/lib/startup-init.js +30 -4
- package/dist/lib/token-lifecycle.js +2 -1
- package/dist/lib/token-log-id.d.ts +18 -0
- package/dist/lib/token-log-id.js +84 -0
- package/dist/lib/two-factor-verify-failure.d.ts +78 -0
- package/dist/lib/two-factor-verify-failure.js +220 -0
- package/dist/middleware/create-middleware.d.ts +4 -1
- package/dist/middleware/create-middleware.js +79 -8
- package/dist/pages/login/page.js +5 -0
- package/dist/pages/recovery/page.js +14 -4
- package/dist/pages/test-env/JwtInspectPage.js +7 -149
- package/dist/pages/test-env/RefreshTokenPage.js +4 -1
- package/dist/pages/test-env/redact-credentials.d.ts +6 -0
- package/dist/pages/test-env/redact-credentials.js +101 -0
- package/dist/pages/verify-code/page.js +6 -3
- package/dist/routes/auth/logout.d.ts +2 -1
- package/dist/routes/auth/logout.js +4 -2
- package/dist/routes/auth/session.d.ts +0 -3
- package/dist/routes/auth/session.js +16 -10
- package/dist/routes/auth/settings.js +4 -1
- package/dist/routes/auth/viability.d.ts +3 -2
- package/dist/routes/auth/viability.js +11 -7
- package/dist/server/auth.d.ts +36 -294
- package/dist/server/auth.js +32 -31
- package/dist/server/decode-session.js +15 -26
- package/dist/server/verified-session-token.d.ts +13 -0
- package/dist/server/verified-session-token.js +103 -0
- package/dist/stores/authStore.js +14 -3
- package/dist/types/recovery.d.ts +1 -1
- package/dist/utils/api.d.ts +18 -0
- package/dist/utils/api.js +51 -9
- package/dist/vibe/client.d.ts +132 -16
- package/dist/vibe/client.js +288 -82
- package/dist/vibe/document-unwrap.d.ts +50 -0
- package/dist/vibe/document-unwrap.js +78 -0
- package/dist/vibe/errors.d.ts +17 -0
- package/dist/vibe/errors.js +21 -1
- package/dist/vibe/generic.d.ts +2 -49
- package/dist/vibe/generic.js +17 -78
- package/dist/vibe/index.d.ts +1 -1
- package/dist/vibe/index.js +2 -1
- package/dist/vibe/sessions.js +2 -1
- package/package.json +13 -2
- package/scripts/verify-release-provenance.mjs +410 -0
- package/src/api/auth-handler.ts +5 -3
- package/src/api-handlers/account/change-password.ts +36 -8
- package/src/api-handlers/account/masked-info.ts +2 -1
- package/src/api-handlers/account/recovery/forward.ts +117 -0
- package/src/api-handlers/account/recovery/initiate.ts +3 -0
- package/src/api-handlers/account/recovery/send-code.ts +13 -99
- package/src/api-handlers/account/recovery/verify-code.ts +12 -99
- package/src/api-handlers/account/reset-password.ts +30 -1
- package/src/api-handlers/account/send-code.ts +3 -9
- package/src/api-handlers/account/update-phone.ts +6 -1
- package/src/api-handlers/account/validate-password.ts +3 -1
- package/src/api-handlers/account/verify-email.ts +4 -9
- package/src/api-handlers/account/verify-sms.ts +4 -9
- package/src/api-handlers/admin/analytics.ts +13 -5
- package/src/api-handlers/admin/audit.ts +13 -5
- package/src/api-handlers/admin/redis-sessions.ts +16 -8
- package/src/api-handlers/admin/sessions.ts +15 -7
- package/src/api-handlers/admin/site-logs.ts +39 -16
- package/src/api-handlers/admin/stats.ts +15 -5
- package/src/api-handlers/admin/users.ts +15 -7
- package/src/api-handlers/admin/vibe-data.ts +20 -12
- package/src/api-handlers/auth/jwks.ts +2 -1
- package/src/api-handlers/auth/login.ts +13 -4
- package/src/api-handlers/auth/passkey-signin.ts +4 -0
- package/src/api-handlers/auth/refresh.ts +38 -11
- package/src/api-handlers/auth/signout.ts +2 -1
- package/src/api-handlers/auth/status.ts +4 -1
- package/src/api-handlers/auth/update-session.ts +45 -11
- package/src/api-handlers/auth/validate.ts +4 -2
- package/src/api-handlers/auth/verify-code.ts +3 -2
- package/src/api-handlers/mfa/backup-code-verify.ts +4 -9
- package/src/api-handlers/mfa/backup-codes.ts +2 -1
- package/src/api-handlers/mfa/methods.ts +2 -1
- package/src/api-handlers/mfa/totp-confirm.ts +2 -1
- package/src/api-handlers/mfa/totp-enroll.ts +2 -1
- package/src/api-handlers/mfa/totp-verify.ts +4 -9
- package/src/api-handlers/passkeys/list.ts +2 -1
- package/src/api-handlers/passkeys/manage.ts +2 -1
- package/src/api-handlers/passkeys/register-options.ts +2 -1
- package/src/api-handlers/passkeys/register-verify.ts +2 -1
- package/src/api-handlers/session/refresh-viability.ts +10 -7
- package/src/api-handlers/session/viability.ts +4 -3
- package/src/api-handlers/test/force-expire.ts +5 -3
- package/src/auth/auth-decision.ts +2 -1
- package/src/auth/better-auth.ts +128 -61
- package/src/auth/utils/idp-client.ts +2 -1
- package/src/components/admin/DataBrowserTab.tsx +4 -1
- package/src/components/admin/SessionsTab.tsx +10 -3
- package/src/components/recovery/RequestedStep.tsx +30 -0
- package/src/components/recovery/SetPasswordStep.tsx +24 -3
- package/src/config/env.ts +0 -2
- package/src/hooks/useViabilitySession.ts +2 -1
- package/src/index.ts +4 -0
- package/src/lib/app-slug.ts +50 -0
- package/src/lib/ensure-fresh-access-token.ts +133 -4
- package/src/lib/idp-client-config.ts +98 -45
- package/src/lib/idp-fetch.ts +1 -1
- package/src/lib/never-in-response.ts +64 -0
- package/src/lib/password-rules.ts +50 -0
- package/src/lib/safe-error-response.ts +126 -0
- package/src/lib/session-cookie-shape.ts +23 -22
- package/src/lib/session-store.ts +167 -24
- package/src/lib/startup-init.ts +29 -4
- package/src/lib/token-lifecycle.ts +2 -1
- package/src/lib/token-log-id.ts +69 -0
- package/src/lib/two-factor-verify-failure.ts +248 -0
- package/src/middleware/create-middleware.ts +97 -9
- package/src/pages/login/page.tsx +4 -0
- package/src/pages/recovery/page.tsx +21 -4
- package/src/pages/test-env/JwtInspectPage.tsx +11 -233
- package/src/pages/test-env/RefreshTokenPage.tsx +9 -4
- package/src/pages/test-env/redact-credentials.ts +96 -0
- package/src/pages/verify-code/page.tsx +6 -3
- package/src/routes/auth/logout.ts +4 -2
- package/src/routes/auth/session.ts +16 -10
- package/src/routes/auth/settings.ts +5 -1
- package/src/routes/auth/viability.ts +11 -7
- package/src/server/auth.ts +33 -35
- package/src/server/decode-session.ts +16 -26
- package/src/server/verified-session-token.ts +103 -0
- package/src/stores/authStore.ts +16 -5
- package/src/types/recovery.ts +1 -1
- package/src/utils/api.ts +47 -8
- package/src/vibe/client.ts +378 -91
- package/src/vibe/document-unwrap.ts +103 -0
- package/src/vibe/errors.ts +24 -0
- package/src/vibe/generic.ts +8 -100
- package/src/vibe/index.ts +1 -0
- package/src/vibe/sessions.ts +2 -1
|
@@ -174,7 +174,10 @@ function DataBrowserTab({ isDark = true, apiBasePath = '/api/admin/vibe', focusC
|
|
|
174
174
|
});
|
|
175
175
|
if (!response.ok) {
|
|
176
176
|
const errData = await response.json().catch(() => ({}));
|
|
177
|
-
|
|
177
|
+
// PAY-1425: the handler returns a fixed message plus a correlationId (never upstream
|
|
178
|
+
// text). Surface the id so an admin can quote it to an operator.
|
|
179
|
+
const ref = errData.error?.correlationId ? ` (ref ${errData.error.correlationId})` : '';
|
|
180
|
+
throw new Error(`${errData.error?.message || 'Delete failed'}${ref}`);
|
|
178
181
|
}
|
|
179
182
|
setSuccess(`Record ${id} deleted`);
|
|
180
183
|
setTimeout(() => setSuccess(null), 3000);
|
|
@@ -7,6 +7,12 @@ const react_1 = require("react");
|
|
|
7
7
|
// -----------------------------------------------------------------------------
|
|
8
8
|
// COMPONENT
|
|
9
9
|
// -----------------------------------------------------------------------------
|
|
10
|
+
// PAY-1429: admin handlers return a fixed message plus a correlationId (never upstream or
|
|
11
|
+
// exception text). Surface the id so an admin can quote it to an operator.
|
|
12
|
+
function errorWithRef(data, fallback) {
|
|
13
|
+
const msg = typeof data?.error === 'string' ? data.error : fallback;
|
|
14
|
+
return data?.correlationId ? `${msg} (ref ${data.correlationId})` : msg;
|
|
15
|
+
}
|
|
10
16
|
function SessionsTab({ isDark = true, apiBasePath = '/api/admin/sessions' }) {
|
|
11
17
|
const [sessions, setSessions] = (0, react_1.useState)([]);
|
|
12
18
|
const [isLoading, setIsLoading] = (0, react_1.useState)(false);
|
|
@@ -41,7 +47,7 @@ function SessionsTab({ isDark = true, apiBasePath = '/api/admin/sessions' }) {
|
|
|
41
47
|
setLastRefresh(new Date());
|
|
42
48
|
}
|
|
43
49
|
else {
|
|
44
|
-
setError(data
|
|
50
|
+
setError(errorWithRef(data, 'Failed to fetch sessions'));
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
catch (err) {
|
|
@@ -91,7 +97,7 @@ function SessionsTab({ isDark = true, apiBasePath = '/api/admin/sessions' }) {
|
|
|
91
97
|
await fetchStats();
|
|
92
98
|
}
|
|
93
99
|
else {
|
|
94
|
-
alert(data
|
|
100
|
+
alert(errorWithRef(data, 'Failed to revoke session'));
|
|
95
101
|
}
|
|
96
102
|
}
|
|
97
103
|
catch (err) {
|
|
@@ -120,7 +126,7 @@ function SessionsTab({ isDark = true, apiBasePath = '/api/admin/sessions' }) {
|
|
|
120
126
|
await fetchStats();
|
|
121
127
|
}
|
|
122
128
|
else {
|
|
123
|
-
alert(data
|
|
129
|
+
alert(errorWithRef(data, 'Failed to revoke sessions'));
|
|
124
130
|
}
|
|
125
131
|
}
|
|
126
132
|
catch (err) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
onGoToLogin: () => void;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* PAY-1588 (rigpert 60711): what a caller sees when the recovery request produced NO recovery session - an unknown
|
|
6
|
+
* address, a locked account, a throttled caller. ONE sentence, identical for all of them, so the screen cannot be used
|
|
7
|
+
* to tell which. It must not claim a reset happened (that is CompleteStep, reached only after a successful reset) and it
|
|
8
|
+
* must not name a cause.
|
|
9
|
+
*/
|
|
10
|
+
export declare function RequestedStep({ onGoToLogin }: Props): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.RequestedStep = RequestedStep;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
/**
|
|
7
|
+
* PAY-1588 (rigpert 60711): what a caller sees when the recovery request produced NO recovery session - an unknown
|
|
8
|
+
* address, a locked account, a throttled caller. ONE sentence, identical for all of them, so the screen cannot be used
|
|
9
|
+
* to tell which. It must not claim a reset happened (that is CompleteStep, reached only after a successful reset) and it
|
|
10
|
+
* must not name a cause.
|
|
11
|
+
*/
|
|
12
|
+
function RequestedStep({ onGoToLogin }) {
|
|
13
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "bg-white border border-gray-300 rounded p-8 text-center", children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-2xl font-semibold text-gray-900 mb-2", children: "Check your email" }), (0, jsx_runtime_1.jsx)("p", { className: "text-gray-600 mb-6", children: "If an account exists for that email, we've sent instructions to reset your password." }), (0, jsx_runtime_1.jsx)("button", { onClick: onGoToLogin, className: "w-full py-2 px-4 border border-gray-300 rounded text-gray-900 font-medium hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400", children: "Back to sign in" })] }));
|
|
14
|
+
}
|
|
@@ -4,17 +4,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
exports.SetPasswordStep = SetPasswordStep;
|
|
5
5
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
6
|
const react_1 = require("react");
|
|
7
|
+
const usePasswordValidation_1 = require("../../hooks/usePasswordValidation");
|
|
8
|
+
const password_rules_1 = require("../../lib/password-rules");
|
|
7
9
|
function SetPasswordStep({ onSubmit, loading }) {
|
|
8
10
|
const [password, setPassword] = (0, react_1.useState)('');
|
|
9
11
|
const [confirmPassword, setConfirmPassword] = (0, react_1.useState)('');
|
|
10
12
|
const [showPassword, setShowPassword] = (0, react_1.useState)(false);
|
|
11
13
|
const [showConfirmPassword, setShowConfirmPassword] = (0, react_1.useState)(false);
|
|
12
14
|
const passwordsMatch = password === confirmPassword && password !== '';
|
|
15
|
+
// PAY-1591: ADVISORY only. It shows the rules and what is missing; it never disables submit, because the validator
|
|
16
|
+
// may be down and the IdP is the authority (a dead validator must not trap a user who already spent their code).
|
|
17
|
+
const { setPassword: validateAsYouType, policy, failedRequirements } = (0, usePasswordValidation_1.usePasswordValidation)();
|
|
18
|
+
// The policy only comes back with a validation, so ask once on mount with a one-character probe (it never leaves the
|
|
19
|
+
// policy question; the answer is what the rules line is built from). Until it lands the line says the rules are coming.
|
|
20
|
+
(0, react_1.useEffect)(() => { validateAsYouType('a'); }, [validateAsYouType]);
|
|
21
|
+
const missing = failedRequirements.filter((r) => !/^(Unable to validate|Password validation)/i.test(r));
|
|
13
22
|
const handleSubmit = (e) => {
|
|
14
23
|
e.preventDefault();
|
|
15
24
|
if (passwordsMatch) {
|
|
16
25
|
onSubmit(password, confirmPassword);
|
|
17
26
|
}
|
|
18
27
|
};
|
|
19
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "bg-white border border-gray-300 rounded p-8", children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-xl font-semibold text-gray-900 mb-2", children: "Set New Password" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-gray-600 mb-
|
|
28
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "bg-white border border-gray-300 rounded p-8", children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-xl font-semibold text-gray-900 mb-2", children: "Set New Password" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-gray-600 mb-2", children: "Choose a strong password for your account." }), (0, jsx_runtime_1.jsx)("p", { "data-testid": "password-rules", className: "text-sm text-gray-700 mb-6", children: (0, password_rules_1.passwordRulesText)(policy) }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "password", className: "block text-sm font-medium text-gray-900 mb-1", children: "New Password" }), (0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("input", { type: showPassword ? 'text' : 'password', id: "password", value: password, onChange: (e) => { setPassword(e.target.value); validateAsYouType(e.target.value); }, required: true, disabled: loading, className: "w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-gray-900 focus:outline-none disabled:opacity-50 disabled:bg-gray-50 pr-10" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setShowPassword(!showPassword), className: "absolute right-2 top-1/2 -translate-y-1/2 text-gray-600 hover:text-gray-900", children: showPassword ? '👁️' : '👁️🗨️' })] })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "confirmPassword", className: "block text-sm font-medium text-gray-900 mb-1", children: "Confirm New Password" }), (0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("input", { type: showConfirmPassword ? 'text' : 'password', id: "confirmPassword", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), required: true, disabled: loading, className: "w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-gray-900 focus:outline-none disabled:opacity-50 disabled:bg-gray-50 pr-10" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setShowConfirmPassword(!showConfirmPassword), className: "absolute right-2 top-1/2 -translate-y-1/2 text-gray-600 hover:text-gray-900", children: showConfirmPassword ? '👁️' : '👁️🗨️' })] })] }), password && missing.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { "data-testid": "password-missing", className: "text-sm text-gray-700 list-disc pl-5", children: missing.map((r) => ((0, jsx_runtime_1.jsx)("li", { children: r }, r))) })), !passwordsMatch && confirmPassword && ((0, jsx_runtime_1.jsx)("div", { className: "bg-red-50 border border-red-200 rounded p-3", children: (0, jsx_runtime_1.jsx)("p", { className: "text-red-700 text-sm", children: "Passwords do not match" }) })), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: !passwordsMatch || loading, className: "w-full py-2 px-4 border border-gray-300 rounded text-gray-900 font-medium hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-900 disabled:opacity-50 disabled:cursor-not-allowed", children: loading ? 'Resetting Password...' : 'Reset Password' })] })] }));
|
|
20
29
|
}
|
package/dist/config/env.d.ts
CHANGED
|
@@ -19,8 +19,6 @@ export declare const API_ENDPOINTS: {
|
|
|
19
19
|
readonly profile: "/api/account/profile";
|
|
20
20
|
readonly updateProfile: "/api/account/profile";
|
|
21
21
|
readonly maskedInfo: "/api/account/masked-info";
|
|
22
|
-
readonly sendResetCode: "/api/account/send-reset-code";
|
|
23
|
-
readonly verifyResetCode: "/api/account/verify-reset-code";
|
|
24
22
|
readonly resetPassword: "/api/account/reset-password";
|
|
25
23
|
readonly changePassword: "/api/account/change-password";
|
|
26
24
|
readonly validatePassword: "/api/account/validate-password";
|
package/dist/config/env.js
CHANGED
|
@@ -29,8 +29,6 @@ exports.API_ENDPOINTS = {
|
|
|
29
29
|
profile: '/api/account/profile',
|
|
30
30
|
updateProfile: '/api/account/profile',
|
|
31
31
|
maskedInfo: '/api/account/masked-info',
|
|
32
|
-
sendResetCode: '/api/account/send-reset-code',
|
|
33
|
-
verifyResetCode: '/api/account/verify-reset-code',
|
|
34
32
|
resetPassword: '/api/account/reset-password',
|
|
35
33
|
changePassword: '/api/account/change-password',
|
|
36
34
|
validatePassword: '/api/account/validate-password'
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { AuthProvider, useAuthConfig, useAuthMode, useFederatedProviders, useFed
|
|
|
5
5
|
export type { AuthConfig } from './types/auth';
|
|
6
6
|
export { makeAuthDecision } from './auth/auth-decision';
|
|
7
7
|
export { isUnauthenticatedRoute, configurePublicRoutes, getRouteConfig } from './auth/route-config';
|
|
8
|
+
export { safeCallbackPath, isSafeCallbackPath, resolveCallbackDestination, DEFAULT_CALLBACK } from './auth/safe-callback';
|
|
8
9
|
export { createMvpMiddleware } from './middleware/create-middleware';
|
|
9
10
|
export { UserAvatarMenu, MobileNavDrawer } from './components/account';
|
|
10
11
|
export type { UserAvatarMenuProps, MobileNavDrawerProps, NavItem, NavSection } from './components/account';
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// - Better Auth: import { createBetterAuthInstance } from '@payez/next-mvp/auth/better-auth'
|
|
9
9
|
// - Server auth: import { getSession } from '@payez/next-mvp/server/auth'
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.queryAuditLog = exports.writeAuditLog = exports.getHealthMetrics = exports.getErrorMetrics = exports.useAdminAnalytics = exports.useAuditLog = exports.useHealthMetrics = exports.useErrorMetrics = exports.AdminAnalyticsLayout = exports.AuditLogViewer = exports.HealthMetricsCard = exports.ErrorMetricsCard = exports.MobileNavDrawer = exports.UserAvatarMenu = exports.createMvpMiddleware = exports.getRouteConfig = exports.configurePublicRoutes = exports.isUnauthenticatedRoute = exports.makeAuthDecision = exports.useTraditionalAuthEnabled = exports.useFederatedAuthEnabled = exports.useFederatedProviders = exports.useAuthMode = exports.useAuthConfig = exports.AuthProvider = exports.useAnonSession = exports.fetchWithAuth = void 0;
|
|
11
|
+
exports.queryAuditLog = exports.writeAuditLog = exports.getHealthMetrics = exports.getErrorMetrics = exports.useAdminAnalytics = exports.useAuditLog = exports.useHealthMetrics = exports.useErrorMetrics = exports.AdminAnalyticsLayout = exports.AuditLogViewer = exports.HealthMetricsCard = exports.ErrorMetricsCard = exports.MobileNavDrawer = exports.UserAvatarMenu = exports.createMvpMiddleware = exports.DEFAULT_CALLBACK = exports.resolveCallbackDestination = exports.isSafeCallbackPath = exports.safeCallbackPath = exports.getRouteConfig = exports.configurePublicRoutes = exports.isUnauthenticatedRoute = exports.makeAuthDecision = exports.useTraditionalAuthEnabled = exports.useFederatedAuthEnabled = exports.useFederatedProviders = exports.useAuthMode = exports.useAuthConfig = exports.AuthProvider = exports.useAnonSession = exports.fetchWithAuth = void 0;
|
|
12
12
|
// Client-safe exports only
|
|
13
13
|
// Client-side utilities
|
|
14
14
|
var fetch_with_auth_1 = require("./client/fetch-with-auth");
|
|
@@ -31,6 +31,13 @@ var route_config_1 = require("./auth/route-config");
|
|
|
31
31
|
Object.defineProperty(exports, "isUnauthenticatedRoute", { enumerable: true, get: function () { return route_config_1.isUnauthenticatedRoute; } });
|
|
32
32
|
Object.defineProperty(exports, "configurePublicRoutes", { enumerable: true, get: function () { return route_config_1.configurePublicRoutes; } });
|
|
33
33
|
Object.defineProperty(exports, "getRouteConfig", { enumerable: true, get: function () { return route_config_1.getRouteConfig; } });
|
|
34
|
+
// The ONE callback-target guard (N7 / PAY-1989): apps import this instead of
|
|
35
|
+
// copying it. Exposed from the main index AND as the subpath ./auth/safe-callback.
|
|
36
|
+
var safe_callback_1 = require("./auth/safe-callback");
|
|
37
|
+
Object.defineProperty(exports, "safeCallbackPath", { enumerable: true, get: function () { return safe_callback_1.safeCallbackPath; } });
|
|
38
|
+
Object.defineProperty(exports, "isSafeCallbackPath", { enumerable: true, get: function () { return safe_callback_1.isSafeCallbackPath; } });
|
|
39
|
+
Object.defineProperty(exports, "resolveCallbackDestination", { enumerable: true, get: function () { return safe_callback_1.resolveCallbackDestination; } });
|
|
40
|
+
Object.defineProperty(exports, "DEFAULT_CALLBACK", { enumerable: true, get: function () { return safe_callback_1.DEFAULT_CALLBACK; } });
|
|
34
41
|
var create_middleware_1 = require("./middleware/create-middleware");
|
|
35
42
|
Object.defineProperty(exports, "createMvpMiddleware", { enumerable: true, get: function () { return create_middleware_1.createMvpMiddleware; } });
|
|
36
43
|
// Account Components
|
package/dist/lib/app-slug.d.ts
CHANGED
|
@@ -14,6 +14,27 @@
|
|
|
14
14
|
* Caches the result for performance.
|
|
15
15
|
*/
|
|
16
16
|
export declare function getAppSlug(): string;
|
|
17
|
+
/**
|
|
18
|
+
* The session cookie name has TWO producers: better-auth mints `${idpConfig.clientSlug || getAppSlug()}.session-token`, while
|
|
19
|
+
* the gate and every reader use getSessionCookieName() = getAppSlug() = APP_SLUG || CLIENT_ID. When the IdP's slug is known and
|
|
20
|
+
* differs, the gate looks for a cookie that was never set and every better-auth (Google) session bounces to
|
|
21
|
+
* /sign-in?error=SessionInvalid straight after a successful 2FA (rigpert 60743, live on :4797 with CLIENT_ID=idp-admin-ui-graft
|
|
22
|
+
* against clientSlug idp_admin_ui_graft). FAIL CLOSED: refuse rather than run with two names. No IdP slug -> nothing to compare.
|
|
23
|
+
*/
|
|
24
|
+
export declare function slugMismatchReason(idpClientSlug: string | null | undefined): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* rigpert 60813: when the IdP returns NO client slug for this app (an unknown client: the config fetch failed or came back
|
|
27
|
+
* without one), startup must fail LOUD. Silently defaulting would run with a cookie name nobody verified against the IdP. The
|
|
28
|
+
* startup path swallows a failed config fetch when BETTER_AUTH_SECRET is supplied by env, so without this an unregistered
|
|
29
|
+
* CLIENT_ID (e.g. payez_idp_customer_web, absent on dev-93) would boot 'fine'.
|
|
30
|
+
*/
|
|
31
|
+
export declare function assertIdpSlugKnown(idpClientSlug: string | null | undefined): void;
|
|
32
|
+
/**
|
|
33
|
+
* The health-endpoint view: a mismatch is always reportable; an UNKNOWN slug is reported only once startup has actually been
|
|
34
|
+
* attempted (before that nothing is known yet and health must not go red on a pod that has not started initialising).
|
|
35
|
+
*/
|
|
36
|
+
export declare function slugStateReason(idpClientSlug: string | null | undefined, startupAttempted: boolean): string | null;
|
|
37
|
+
export declare function assertAppSlugMatchesIdp(idpClientSlug: string | null | undefined): void;
|
|
17
38
|
/**
|
|
18
39
|
* Clears the cached slug (useful for testing).
|
|
19
40
|
*/
|
package/dist/lib/app-slug.js
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.getAppSlug = getAppSlug;
|
|
15
|
+
exports.slugMismatchReason = slugMismatchReason;
|
|
16
|
+
exports.assertIdpSlugKnown = assertIdpSlugKnown;
|
|
17
|
+
exports.slugStateReason = slugStateReason;
|
|
18
|
+
exports.assertAppSlugMatchesIdp = assertAppSlugMatchesIdp;
|
|
15
19
|
exports.clearSlugCache = clearSlugCache;
|
|
16
20
|
exports.getSessionPrefix = getSessionPrefix;
|
|
17
21
|
exports.getAnonPrefix = getAnonPrefix;
|
|
@@ -37,6 +41,53 @@ function getAppSlug() {
|
|
|
37
41
|
cachedSlug = process.env.APP_SLUG || process.env.CLIENT_ID || 'payez';
|
|
38
42
|
return cachedSlug;
|
|
39
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The session cookie name has TWO producers: better-auth mints `${idpConfig.clientSlug || getAppSlug()}.session-token`, while
|
|
46
|
+
* the gate and every reader use getSessionCookieName() = getAppSlug() = APP_SLUG || CLIENT_ID. When the IdP's slug is known and
|
|
47
|
+
* differs, the gate looks for a cookie that was never set and every better-auth (Google) session bounces to
|
|
48
|
+
* /sign-in?error=SessionInvalid straight after a successful 2FA (rigpert 60743, live on :4797 with CLIENT_ID=idp-admin-ui-graft
|
|
49
|
+
* against clientSlug idp_admin_ui_graft). FAIL CLOSED: refuse rather than run with two names. No IdP slug -> nothing to compare.
|
|
50
|
+
*/
|
|
51
|
+
function slugMismatchReason(idpClientSlug) {
|
|
52
|
+
if (!idpClientSlug)
|
|
53
|
+
return null;
|
|
54
|
+
const gateSlug = getAppSlug();
|
|
55
|
+
return idpClientSlug !== gateSlug ? `SLUG_MISMATCH: cookie=${idpClientSlug} gate=${gateSlug}` : null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* rigpert 60813: when the IdP returns NO client slug for this app (an unknown client: the config fetch failed or came back
|
|
59
|
+
* without one), startup must fail LOUD. Silently defaulting would run with a cookie name nobody verified against the IdP. The
|
|
60
|
+
* startup path swallows a failed config fetch when BETTER_AUTH_SECRET is supplied by env, so without this an unregistered
|
|
61
|
+
* CLIENT_ID (e.g. payez_idp_customer_web, absent on dev-93) would boot 'fine'.
|
|
62
|
+
*/
|
|
63
|
+
function assertIdpSlugKnown(idpClientSlug) {
|
|
64
|
+
if (idpClientSlug)
|
|
65
|
+
return;
|
|
66
|
+
throw new Error(`[FATAL] The IdP returned no client slug for this app (gate slug "${getAppSlug()}"). Either CLIENT_ID/APP_SLUG names a client ` +
|
|
67
|
+
`the IdP does not know, or its config could not be fetched. Refusing to start with an unverified session cookie name.`);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The health-endpoint view: a mismatch is always reportable; an UNKNOWN slug is reported only once startup has actually been
|
|
71
|
+
* attempted (before that nothing is known yet and health must not go red on a pod that has not started initialising).
|
|
72
|
+
*/
|
|
73
|
+
function slugStateReason(idpClientSlug, startupAttempted) {
|
|
74
|
+
const mismatch = slugMismatchReason(idpClientSlug);
|
|
75
|
+
if (mismatch)
|
|
76
|
+
return mismatch;
|
|
77
|
+
if (startupAttempted && !idpClientSlug)
|
|
78
|
+
return `SLUG_UNKNOWN: the IdP returned no client slug for this app gate=${getAppSlug()}`;
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function assertAppSlugMatchesIdp(idpClientSlug) {
|
|
82
|
+
if (!idpClientSlug)
|
|
83
|
+
return;
|
|
84
|
+
const gateSlug = getAppSlug();
|
|
85
|
+
if (slugMismatchReason(idpClientSlug) !== null) {
|
|
86
|
+
throw new Error(`[FATAL] Session cookie name would have two sources: the IdP client slug is "${idpClientSlug}" but this app's slug ` +
|
|
87
|
+
`(APP_SLUG, else CLIENT_ID) is "${gateSlug}". better-auth would set "${idpClientSlug}.session-token" while the gate reads ` +
|
|
88
|
+
`"${gateSlug}.session-token", so every better-auth session would be rejected. Set APP_SLUG (or CLIENT_ID) to "${idpClientSlug}".`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
40
91
|
/**
|
|
41
92
|
* Clears the cached slug (useful for testing).
|
|
42
93
|
*/
|
|
@@ -17,6 +17,32 @@ export type EnsureFreshResult = {
|
|
|
17
17
|
accessTokenExpires: number;
|
|
18
18
|
/** True if we refreshed (or a concurrent refresh completed); false if the stored token was already fresh. */
|
|
19
19
|
refreshed: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* CARD 320072. Is the token in THIS result actually in the session store?
|
|
22
|
+
*
|
|
23
|
+
* ON THIS BRANCH OF THE UNION IT IS ALWAYS `true`, AND THAT IS THE POINT.
|
|
24
|
+
* Every `ok: true` return either read the token back from the store or
|
|
25
|
+
* wrote it and confirmed the write. An unpersisted refresh does not reach
|
|
26
|
+
* an `ok: true` result at all — it returns `ok:false, SESSION_PERSIST_FAILED`
|
|
27
|
+
* from the other branch below.
|
|
28
|
+
*
|
|
29
|
+
* READ THIS BEFORE YOU DELETE THE FIELD. It is declared `boolean` rather
|
|
30
|
+
* than the literal `true` on purpose: it is the assertable witness that the
|
|
31
|
+
* escalation is still wired. If someone later re-weakens the persist site
|
|
32
|
+
* back to reporting success, this field is where that regression becomes
|
|
33
|
+
* visible to a test instead of invisible to everyone. Cases 2 and 7 in
|
|
34
|
+
* test/ensure-fresh-persist-failure.test.cjs assert on it.
|
|
35
|
+
*
|
|
36
|
+
* WHAT THIS COMMENT USED TO SAY, AND WHY IT IS GONE. It documented `false`
|
|
37
|
+
* as a live value on an ok:true result — "we hold a valid token that was
|
|
38
|
+
* not persisted" — because that WAS the contract before this merge, when
|
|
39
|
+
* the dropped write was reported as a success with a flag on it. Blending
|
|
40
|
+
* in the escalation made that sentence describe a state the code can no
|
|
41
|
+
* longer produce. A docblock asserting an impossible semantic is exactly
|
|
42
|
+
* the defect f5ac6dc3 was written to fix, one file over; reproducing it
|
|
43
|
+
* here while merging that fix would be absurd.
|
|
44
|
+
*/
|
|
45
|
+
persisted: boolean;
|
|
20
46
|
} | {
|
|
21
47
|
ok: false;
|
|
22
48
|
code: string;
|
|
@@ -66,7 +66,8 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
|
|
|
66
66
|
const effectiveExpires = jwtExpires > 0 ? Math.min(redisExpires || jwtExpires, jwtExpires) : redisExpires;
|
|
67
67
|
const msUntilExpiry = effectiveExpires - now;
|
|
68
68
|
if (msUntilExpiry > safetyWindowMs) {
|
|
69
|
-
|
|
69
|
+
// Durable by construction: this token came OUT of the store and we wrote nothing.
|
|
70
|
+
return { ok: true, accessToken: storedToken, accessTokenExpires: effectiveExpires, refreshed: false, persisted: true };
|
|
70
71
|
}
|
|
71
72
|
if (!session.idpRefreshToken) {
|
|
72
73
|
return {
|
|
@@ -103,6 +104,8 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
|
|
|
103
104
|
accessToken: after.idpAccessToken,
|
|
104
105
|
accessTokenExpires: after.idpAccessTokenExpires,
|
|
105
106
|
refreshed: true,
|
|
107
|
+
// Read back from the store after the winner's refresh - durable.
|
|
108
|
+
persisted: true,
|
|
106
109
|
};
|
|
107
110
|
}
|
|
108
111
|
}
|
|
@@ -140,6 +143,8 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
|
|
|
140
143
|
accessToken: latest.idpAccessToken,
|
|
141
144
|
accessTokenExpires: latest.idpAccessTokenExpires,
|
|
142
145
|
refreshed: true,
|
|
146
|
+
// Post-lock re-read: another caller already refreshed and stored this.
|
|
147
|
+
persisted: true,
|
|
143
148
|
};
|
|
144
149
|
}
|
|
145
150
|
}
|
|
@@ -229,12 +234,37 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
|
|
|
229
234
|
// between pre-lock read and our send would be misjudged.
|
|
230
235
|
const tokenUnchanged = !!sentRefreshToken && postRefresh?.idpRefreshToken === sentRefreshToken;
|
|
231
236
|
if (tokenUnchanged) {
|
|
232
|
-
|
|
237
|
+
/**
|
|
238
|
+
* 320072: this is the OTHER updateSession call in this file, and it
|
|
239
|
+
* does NOT escalate on a null return the way the persist site below
|
|
240
|
+
* does. Checked, not overlooked: reaching here requires
|
|
241
|
+
* `tokenUnchanged`, which requires `postRefresh?.idpRefreshToken` to
|
|
242
|
+
* equal the token we sent — so the CAS read three lines up already
|
|
243
|
+
* proved the row exists. A missing row makes tokenUnchanged false and
|
|
244
|
+
* routes to the else branch instead, so the dropped-write path is
|
|
245
|
+
* very nearly unreachable here.
|
|
246
|
+
*
|
|
247
|
+
* "Very nearly" is why the return IS still captured. The residual both
|
|
248
|
+
* halves of this merge identified is real: the row can expire between
|
|
249
|
+
* that CAS read and this write, in which case the dead refresh token is
|
|
250
|
+
* NOT cleared and the "we discarded it" claim in the error below is
|
|
251
|
+
* false. That costs nothing to observe and cannot be inferred from any
|
|
252
|
+
* other line, so it is warned rather than assumed — see `if (!cleared)`
|
|
253
|
+
* immediately below. The RESULT is unchanged either way: the session is
|
|
254
|
+
* unusable and we return the same non-ok payload.
|
|
255
|
+
*/
|
|
256
|
+
const cleared = await (0, session_store_1.updateSession)(sessionToken, {
|
|
233
257
|
idpRefreshToken: '',
|
|
234
258
|
idpRefreshTokenExpires: undefined,
|
|
235
259
|
refreshTokenClearedAt: Date.now(),
|
|
236
260
|
refreshTokenClearedReason: `IDP_DISCARD_TOKEN:${code}`,
|
|
237
261
|
});
|
|
262
|
+
if (!cleared) {
|
|
263
|
+
console.warn('[ENSURE-FRESH] discard-token clear did not persist - no canonical session row', {
|
|
264
|
+
code,
|
|
265
|
+
requestId,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
238
268
|
}
|
|
239
269
|
else {
|
|
240
270
|
// Concurrent refresh already rotated the token — our 401 is stale. Return the
|
|
@@ -247,6 +277,8 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
|
|
|
247
277
|
accessToken: postRefresh.idpAccessToken,
|
|
248
278
|
accessTokenExpires: postRefresh.idpAccessTokenExpires,
|
|
249
279
|
refreshed: true,
|
|
280
|
+
// `postRefresh` is a store read, so this token is by definition stored.
|
|
281
|
+
persisted: true,
|
|
250
282
|
};
|
|
251
283
|
}
|
|
252
284
|
}
|
|
@@ -301,7 +333,14 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
|
|
|
301
333
|
const acrLevel = String(decoded?.acr || session.authenticationLevel || '1');
|
|
302
334
|
const hasNewRefresh = typeof newRefresh === 'string' && newRefresh.length > 0;
|
|
303
335
|
const newKid = (0, token_utils_1.extractKidFromToken)(newAccess);
|
|
304
|
-
|
|
336
|
+
// *** CARD 320072. CAPTURE THIS RETURN. ***
|
|
337
|
+
// `updateSession` returns null - and writes nothing - when the canonical
|
|
338
|
+
// session row is missing, which happens whenever the BA fallback above is what
|
|
339
|
+
// resolved this session. Discarding the return is what let this function
|
|
340
|
+
// report `ok:true, refreshed:true` for a refresh it had just thrown away.
|
|
341
|
+
// The full rationale for what we DO about it is at the `if (!persisted)`
|
|
342
|
+
// block below; this line only makes the answer available.
|
|
343
|
+
const persisted = await (0, session_store_1.updateSession)(sessionToken, {
|
|
305
344
|
...session,
|
|
306
345
|
idpAccessToken: newAccess,
|
|
307
346
|
idpAccessTokenExpires: accessTokenExpires,
|
|
@@ -320,7 +359,69 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
|
|
|
320
359
|
mfaExpiresAt: decoded?.mfa_expires ? parseInt(decoded.mfa_expires) * 1000 : session.mfaExpiresAt,
|
|
321
360
|
mfaValidityHours: decoded?.mfa_validity_hours ? parseInt(decoded.mfa_validity_hours) : session.mfaValidityHours,
|
|
322
361
|
});
|
|
323
|
-
|
|
362
|
+
/**
|
|
363
|
+
* 320072: DO NOT REPORT SUCCESS FOR A WRITE THAT WAS DISCARDED.
|
|
364
|
+
*
|
|
365
|
+
* `updateSession` returns null when the canonical session row is absent,
|
|
366
|
+
* which silently throws away EVERYTHING above — the new access token, the
|
|
367
|
+
* rotated refresh token, decodedAccessToken, authenticationMethods,
|
|
368
|
+
* mfaVerified, mfaCompletedAt, mfaExpiresAt. This return value was
|
|
369
|
+
* previously ignored, and the line below then reported
|
|
370
|
+
* `ok: true, refreshed: true`.
|
|
371
|
+
*
|
|
372
|
+
* WHY THAT IS WORSE THAN A PLAIN LOST WRITE. By this point the exchange at
|
|
373
|
+
* the IDP has ALREADY HAPPENED, so if refresh tokens are single-use the
|
|
374
|
+
* token we sent is now spent and its replacement is the thing we just
|
|
375
|
+
* dropped. The stored credential is therefore dead, and every later attempt
|
|
376
|
+
* re-sends that same dead token and fails. The user is bounced to LOGIN —
|
|
377
|
+
* not to verify-code — with nothing in the logs naming the cause.
|
|
378
|
+
*
|
|
379
|
+
* It did not even surface as a wrong answer to our one caller: server/auth.ts
|
|
380
|
+
* calls getRedisSession immediately after us, which is this same `getSession`,
|
|
381
|
+
* so it finds the same missing row and returns a generic NO_SESSION one frame
|
|
382
|
+
* later. The false `ok: true` was thus invisible AND the real cause — a
|
|
383
|
+
* dropped persist — was reported as an unrelated missing session.
|
|
384
|
+
*
|
|
385
|
+
* Naming it here is the point: the failure is reported at its true cause,
|
|
386
|
+
* with a code that says the refresh succeeded and the PERSIST is what
|
|
387
|
+
* failed. Not retryable — retrying burns another refresh token the same way.
|
|
388
|
+
*
|
|
389
|
+
* This is the card's option (b), deliberately. Option (a) — teaching
|
|
390
|
+
* updateSession to write through to whichever store holds the session,
|
|
391
|
+
* mirroring the read fallback above — is the better end state but changes
|
|
392
|
+
* write-path storage semantics for every caller, which is not this card.
|
|
393
|
+
*
|
|
394
|
+
* AND IT IS ALSO LOGGED, WHICH IS NOT THE SAME THING (BAPert R1134 §7,
|
|
395
|
+
* re-affirmed in R1145 §2c). Returning a named code tells the CALLER. §7
|
|
396
|
+
* requires that `persisted:false` reach a log A HUMAN READS, because the one
|
|
397
|
+
* caller — server/auth.ts — converts every non-ok result into the same
|
|
398
|
+
* generic 401 and keeps no record of which code produced it. Without this
|
|
399
|
+
* line the distinction we just bought at the type level is erased one frame
|
|
400
|
+
* later and no operator ever sees the word PERSIST. The escalation is the
|
|
401
|
+
* contract; the warn is the evidence.
|
|
402
|
+
*/
|
|
403
|
+
if (!persisted) {
|
|
404
|
+
console.warn('[ENSURE-FRESH] refresh succeeded but was NOT persisted - no canonical session row', {
|
|
405
|
+
requestId,
|
|
406
|
+
refreshTokenRotated: hasNewRefresh,
|
|
407
|
+
persisted: false,
|
|
408
|
+
});
|
|
409
|
+
return {
|
|
410
|
+
ok: false,
|
|
411
|
+
code: 'SESSION_PERSIST_FAILED',
|
|
412
|
+
message: 'Token refresh succeeded at the IDP but the session could not be persisted; the rotated tokens were discarded.',
|
|
413
|
+
status: 401,
|
|
414
|
+
terminal: true,
|
|
415
|
+
retryable: false,
|
|
416
|
+
discardToken: true,
|
|
417
|
+
resolution: 'REAUTHENTICATE',
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
// `persisted: true` is not decoration and not a leftover: the `if` above is
|
|
421
|
+
// the only way out for a dropped write, so by construction anything reaching
|
|
422
|
+
// this line WAS written. Keeping the field keeps the ok:true branch of
|
|
423
|
+
// EnsureFreshResult total (Cases 2 and 7 assert on it) — BAPert R1145 §2d.
|
|
424
|
+
return { ok: true, accessToken: newAccess, accessTokenExpires, refreshed: true, persisted: true };
|
|
324
425
|
}
|
|
325
426
|
finally {
|
|
326
427
|
if (weHoldLock) {
|
|
@@ -81,6 +81,26 @@ export interface IDPClientConfig {
|
|
|
81
81
|
branding: BrandingConfig;
|
|
82
82
|
baseClientUrl?: string;
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* The IdP serves the signing secret and every OAuth provider client secret in the client config, to any
|
|
86
|
+
* caller holding a client assertion (PAY-1460). It cannot stop serving them while this package depends on
|
|
87
|
+
* them: every earlier version throws FATAL on an empty config authSecret, whatever the environment says.
|
|
88
|
+
*
|
|
89
|
+
* So both are now resolved ENV FIRST, config second:
|
|
90
|
+
* signing secret BETTER_AUTH_SECRET (canonical), NEXTAUTH_SECRET (legacy alias), then config authSecret.
|
|
91
|
+
* Uses the module-load snapshot above: this module publishes the config value into
|
|
92
|
+
* process.env, so a live read would mistake the IdP's value for the operator's.
|
|
93
|
+
* provider secret <PROVIDER>_CLIENT_SECRET, provider name upper-cased with non-alphanumerics as '_'
|
|
94
|
+
* (GOOGLE_CLIENT_SECRET, APPLE_CLIENT_SECRET, MICROSOFT_CLIENT_SECRET), then config.
|
|
95
|
+
*
|
|
96
|
+
* The FATAL on an empty signing secret is unchanged and still load-bearing: it now fires only when BOTH the
|
|
97
|
+
* environment and the config are empty.
|
|
98
|
+
*
|
|
99
|
+
* Values are never logged. The SOURCE of each is logged once per change. An env-sourced value is never
|
|
100
|
+
* written to the Redis config cache (stripEnvSecrets); a cache read re-applies the environment.
|
|
101
|
+
*/
|
|
102
|
+
export type SecretSource = string;
|
|
103
|
+
export declare function providerSecretEnvVar(provider: string): string;
|
|
84
104
|
/**
|
|
85
105
|
* Get IDP client configuration with multi-tier caching.
|
|
86
106
|
*
|