@payez/next-mvp 4.13.0 → 4.13.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/CHANGELOG.md +12 -0
- package/dist/api-handlers/account/profile.d.ts +2 -39
- package/dist/api-handlers/account/profile.js +5 -2
- package/dist/api-handlers/anon/preferences.d.ts +1 -14
- package/dist/api-handlers/anon/preferences.js +5 -5
- package/dist/api-handlers/health.d.ts +5 -0
- package/dist/api-handlers/health.js +48 -0
- package/dist/api-handlers/session/refresh-viability.d.ts +3 -15
- package/dist/api-handlers/session/refresh-viability.js +5 -2
- package/dist/api-handlers/session/viability.d.ts +3 -8
- package/dist/api-handlers/session/viability.js +5 -2
- package/dist/auth/better-auth-exposure.d.ts +11 -0
- package/dist/auth/better-auth-exposure.js +73 -0
- package/dist/auth/better-auth.js +13 -1
- package/dist/auth/route-config.js +9 -0
- package/dist/lib/no-store.d.ts +13 -0
- package/dist/lib/no-store.js +35 -0
- package/dist/routes/auth/session.d.ts +2 -9
- package/dist/routes/auth/session.js +5 -2
- package/dist/routes/auth/viability.d.ts +3 -54
- package/dist/routes/auth/viability.js +5 -2
- package/dist/routes/health.d.ts +1 -0
- package/dist/routes/health.js +16 -0
- package/dist/routes/mfa/methods.d.ts +2 -2
- package/dist/routes/mfa/methods.js +3 -1
- package/dist/routes/passkeys/list.d.ts +2 -10
- package/dist/routes/passkeys/list.js +4 -2
- package/dist/routes/session/refresh-viability.d.ts +2 -1
- package/dist/routes/session/refresh-viability.js +4 -2
- package/dist/utils/api.d.ts +33 -6
- package/dist/utils/api.js +35 -2
- package/package.json +11 -1
- package/src/api-handlers/account/profile.ts +5 -1
- package/src/api-handlers/anon/preferences.ts +5 -4
- package/src/api-handlers/health.ts +53 -0
- package/src/api-handlers/session/refresh-viability.ts +5 -1
- package/src/api-handlers/session/viability.ts +6 -2
- package/src/auth/better-auth-exposure.ts +69 -0
- package/src/auth/better-auth.ts +15 -1
- package/src/auth/route-config.ts +9 -0
- package/src/lib/no-store.ts +30 -0
- package/src/routes/auth/session.ts +5 -1
- package/src/routes/auth/viability.ts +6 -2
- package/src/routes/health.ts +14 -0
- package/src/routes/mfa/methods.ts +3 -1
- package/src/routes/passkeys/list.ts +5 -1
- package/src/routes/session/refresh-viability.ts +5 -1
- package/src/utils/api.ts +50 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog — @payez/next-mvp
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- **ADDITIVE (MINOR when released): SMS-first sign-up learns the `EMAIL_CONFIRMATION_REQUIRED` step (BAPert 88395 #2, 88454 #3; NextPert 88471).** The server (door 3b) now answers `verify-sms-code` with 200 success, `next_step = EMAIL_CONFIRMATION_REQUIRED` and NO reset token when the email is still unconfirmed. Before this, `accountApi.verifyOnboardingSmsCode` returned `{ success: true, resetToken: '' }` for that answer, which a consumer reads as "verified" and then calls set-password with an EMPTY token. Now: (a) the result type is the exported `SmsCodeVerification` and gains an OPTIONAL `nextStep` (`nextStep` ALONE is read from the nested shapes AND from a flat body; `reset_token`, `email` and `client_key` are still read from the nested shapes only, as before, so a consumer behind a proxy that forwards a flat body gets no token and the helper then says `more_steps`, the safe direction; absent / null / '' = no step; any other value, including `false`, `0` and a non-string, is kept as a non-empty UNRECOGNISED string, never dropped, so a server that means "no step" must send nothing or null); (b) new pure `interpretSmsVerification(result)` returns `verified` (only with a non-empty reset token and no step), `email_confirmation_required` (the package sentence "Your phone is verified. Confirm your email to finish signing up.", no error styling), `more_steps` ("One more step is needed to finish signing up." for ANY other step, and for success with an empty reset token) or `not_verified`; `success: false` WINS over any step (`not_verified`, so a failed answer never says "Your phone is verified"), and for every other answer it tests `nextStep` BEFORE the token; the sentences are the package's own, never the server's text. **The method itself still returns the server's `success` unchanged: a consumer MUST use `interpretSmsVerification` (or test `nextStep` first) and must not treat `success` alone as "verified", because `success: true` with an empty `resetToken` is what the server now sends for the unconfirmed-email case.** No breaking type change: a consumer that ignores `nextStep` compiles unchanged (test S6), and the email variant `verifyOnboardingCode` is untouched. **A consumer must adopt `interpretSmsVerification` (or branch on `nextStep` itself) to show the new screen; until it does, it behaves as before.** There is no SMS-first page in this package: the screen lives in the consuming sites, so the deploy of the server change (PR 85) waits on every live consumer taking this release. Tests: `test/sms-next-step.test.cjs` (32), `test/sms-next-step-mutants.test.cjs` (9 hand mutants, one per branch).
|
|
6
|
+
|
|
7
|
+
## 4.13.1 (to be published under dist-tag `next` only, after 4.13.0; `latest` stays 4.8.9 until Jon says otherwise)
|
|
8
|
+
|
|
9
|
+
A patch: ONE change, the PAY-2994 no-store fix for 8 GET routes (re-cut on GitHub from DevOps PR 1757, BAPert 92969; the DevOps repo is frozen).
|
|
10
|
+
|
|
11
|
+
- **FIX (PAY-2994): eight GET routes now answer `Cache-Control: no-store` on every response.** The portal's BFF no-store ratchet found that these re-exported routes sent no Cache-Control, though their answers carry per-user data and some carry tokens, so a browser or an intermediate cache could keep them. New internal `lib/no-store` (`noStore(handler)`): it sets `no-store` on whatever the handler returns (errors and redirects too), REPLACES a looser existing value, rebuilds a Response whose headers are immutable (e.g. `Response.redirect`) with status, body and every `Set-Cookie` intact, passes the handler's arguments through, and hands a non-Response back untouched. Wrapped: `api-handlers/account/profile` GET, `api-handlers/anon/preferences` GET, `auth/better-auth` `createAuthGetHandler`, `routes/auth/session` GET, `routes/mfa/methods` GET, `routes/passkeys/list` GET, `routes/session/refresh-viability` GET, `routes/auth/viability` GET; and, as the same data under a second export path (NightHawk 92964 Low 1), `api-handlers/session/viability` GET and `api-handlers/session/refresh-viability` GET. No other behaviour changes. Package test `test/next-mvp-pay-2994-no-store.test.cjs` calls the REAL compiled GET of each and a mutant (wrapper removed) goes red.
|
|
12
|
+
- **NOT covered (follow-up, so the next ratchet run is expected, NightHawk 92964 Low 2):** GET answers in `api-handlers/auth/status`, `api-handlers/auth/validate`, `routes/auth/session-reset`, `routes/auth/settings` and `api-handlers/oauth/resume` (a redirect that sets cookies) still send no `no-store`. They were not in the ratchet's finding.
|
|
13
|
+
|
|
3
14
|
## 4.13.0 (2026-11-05; published under dist-tag `next` only, after 4.12.1; `latest` stays 4.8.9 until Jon says otherwise)
|
|
4
15
|
|
|
5
16
|
A minor. Numbered 4.13.0 because everything new is additive or opt-in and no export was removed (two exports were added: `./components/mfa/TotpEnrollPanel` and `./dist/pages/enroll`), but three changes alter behaviour for every consumer and are listed first so nobody is surprised. Built from GitHub `keelsecurity/keelauth-next` main (the code is e0ccb0b9; this release commit changes only this file and the version). Base: 4.12.1 (PR 1500 only). PR numbers are the GitHub-era numbers where the PR was opened after the cutover, else the ADO number. Consumers keep their own pins: nothing here bumps or pins any consumer, and the portal stays on 4.12.1 until its owner says otherwise.
|
|
@@ -42,6 +53,7 @@ Not covered: Safari/Firefox (Chromium only), non-browser clients and browsers th
|
|
|
42
53
|
|
|
43
54
|
## Earlier entries now rolled into 4.13.0 (previously "Unreleased: PAY-2568 / PAY-2279, draft PR; no release, no pin")
|
|
44
55
|
|
|
56
|
+
|
|
45
57
|
- **CHANGE: a failed credential sign-in shows a fixed sentence chosen by a stable code, never the server text.** `lib/sign-in-failure` is one closed table: `invalid_credentials` ("Incorrect email or password.", wrong password, unknown account and any other refusal, identical), `not_a_member` (the IdP code `NotAMemberOfThisApp` at any HTTP status - the public door relays it as 400 - shown only to someone who proved the credentials), `too_many_attempts` (429, `AccountLocked`, `UserLockedOut`, one sentence for all). `/api/auth/admin-login` now answers `code` + the fixed sentence in `error`; the browser client takes the sentence from the table by code and ignores the body text (an unmapped refusal gets one generic sentence, `sign_in_failed`). Each rendered error carries `data-error-code` (form and login page). Allowlisted policy refusals (SsoRequired, InvalidRequest) still show their detail under code `policy_message`, as a plain text node, at most 200 characters (BAPert 85432); no other server text reaches the page. **What changes for a caller:** the 429 sentence and the `?error=CredentialsSignin` sentence changed; a custom UI that read `body.error` still gets a fixed sentence there. Rows: `test/signin-failure-copy.test.cjs` S1-S7 (RED on master, six mutants).
|
|
46
58
|
- **FIX: the code step of a credential sign-in (wrong, unavailable, locked) shows its own sentence again (BAPert 85795).** The code-step branch of `api-handlers/auth/credential-login` sent no `code`, so after the change above every second-factor refusal fell to the generic "Could not sign in. Check your details and try again.", including the "problem on our side, not with your code" answer. The branch now sends `invalid_second_factor` ("That code is not correct. Check the code and try again."), `second_factor_unavailable` (the existing our-side sentence, with the Email-me-a-code offer kept) or `too_many_attempts`, chosen by the existing `classifyTwofaRejection` and never from IdP wording. The lockout sentence now states the window: `Too many sign-in attempts. Try again in N minutes, or reset your password.`, N = the server's `retryAfter` (seconds) rounded up to whole minutes and clamped to 1..60; with no usable number it stays the plain sentence (only a number is ever printed). The password-step 429 no longer invents a `retryAfter` of 60 when the IdP sent no Retry-After. A plain IdP 429 on the code step (a lane rate limit, not a `locked` lockout) has its own code `second_factor_rate_limited`: "Too many attempts. Try again in N minutes." (same N rule; plain fallback "Too many attempts. Wait a few minutes and try again."), so it no longer tells a user to reset their password; it makes no claim that the code is still valid (unproven here), and `twofa-rejection` no longer invents a 60 s `retryAfter`. Sentences lost by the 1719 regression in this lane: the wrong-code sentence, the our-side-fault sentence (with its Email-me-a-code offer text) and the lockout window. Test: `test/second-factor-copy.test.cjs` (T1-T15, mutants RED).
|
|
47
59
|
|
|
@@ -1,41 +1,4 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* GET /api/account/profile - Get user profile
|
|
6
|
-
* PUT /api/account/profile - Update user profile
|
|
7
|
-
*
|
|
8
|
-
* WHY THIS WAS REWRITTEN (2026-08-02, QAPert defect #7)
|
|
9
|
-
* Reported as "/api/account/profile 401s on the same cookie masked-info
|
|
10
|
-
* accepts (200)". Two endpoints disagreeing about one cookie meant two auth
|
|
11
|
-
* implementations, and they were:
|
|
12
|
-
*
|
|
13
|
-
* masked-info.ts idpFetchJSON(req, ...) resolves the caller's token
|
|
14
|
-
* from the server-side session
|
|
15
|
-
* profile.ts req.headers.get('authorization') required an INBOUND bearer
|
|
16
|
-
*
|
|
17
|
-
* A browser sends a session COOKIE and no Authorization header, so the old
|
|
18
|
-
* profile.ts 401'd EVERY browser request by construction. Nothing was revoked
|
|
19
|
-
* and no token was stale — the handler demanded a credential the app does not
|
|
20
|
-
* use.
|
|
21
|
-
*
|
|
22
|
-
* THE OLD AUTH CHECK WAS A VENEER. It tested that the header was PRESENT, never
|
|
23
|
-
* that it was valid, then forwarded it upstream unexamined. Any string at all
|
|
24
|
-
* got past it and the IDP did the real work. It looked like enforcement and
|
|
25
|
-
* enforced nothing.
|
|
26
|
-
*
|
|
27
|
-
* WHAT THE WORKAROUND COST US: consumers could not call this with a cookie, so
|
|
28
|
-
* the graft's useProfile hook fetched the raw IDP access token from
|
|
29
|
-
* /api/session/idp-token INTO BROWSER JAVASCRIPT and re-sent it as a bearer.
|
|
30
|
-
* That defeats the reason idpFetchJSON exists — it keeps the token server-side
|
|
31
|
-
* precisely so client-side script can never hold it. The bearer-only handler
|
|
32
|
-
* did not merely fail; it pushed its callers into exposing the token.
|
|
33
|
-
*
|
|
34
|
-
* Switching to idpFetchJSON makes the cookie work, so the hook's token round
|
|
35
|
-
* trip can be deleted and /api/session/idp-token drops to a single deliberate
|
|
36
|
-
* caller: the jwt-inspect debug page.
|
|
37
|
-
*
|
|
38
|
-
* @package @payez/next-mvp
|
|
39
|
-
*/
|
|
40
|
-
export declare function GET(req: NextRequest): Promise<NextResponse<any>>;
|
|
2
|
+
/** PAY-2994: the profile is one person's data; never cached (noStore). */
|
|
3
|
+
export declare const GET: (req: NextRequest) => Promise<Response>;
|
|
41
4
|
export declare function PUT(req: NextRequest): Promise<NextResponse<any>>;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GET =
|
|
3
|
+
exports.GET = void 0;
|
|
4
4
|
exports.PUT = PUT;
|
|
5
5
|
const server_1 = require("next/server");
|
|
6
6
|
const idp_fetch_1 = require("../../lib/idp-fetch");
|
|
7
7
|
const env_1 = require("../../config/env");
|
|
8
|
+
const no_store_1 = require("../../lib/no-store");
|
|
8
9
|
/**
|
|
9
10
|
* Account Profile API Handler
|
|
10
11
|
*
|
|
@@ -43,7 +44,7 @@ const env_1 = require("../../config/env");
|
|
|
43
44
|
*
|
|
44
45
|
* @package @payez/next-mvp
|
|
45
46
|
*/
|
|
46
|
-
async function
|
|
47
|
+
async function getProfile(req) {
|
|
47
48
|
const result = await (0, idp_fetch_1.idpFetchJSON)(req, `${env_1.ENV_CONFIG.IDP_URL}/api/Account/profile`, {
|
|
48
49
|
method: 'GET',
|
|
49
50
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -61,6 +62,8 @@ async function GET(req) {
|
|
|
61
62
|
}
|
|
62
63
|
return server_1.NextResponse.json(result.json, { status: 200 });
|
|
63
64
|
}
|
|
65
|
+
/** PAY-2994: the profile is one person's data; never cached (noStore). */
|
|
66
|
+
exports.GET = (0, no_store_1.noStore)(getProfile);
|
|
64
67
|
async function PUT(req) {
|
|
65
68
|
let body = '{}';
|
|
66
69
|
try {
|
|
@@ -8,23 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { NextRequest, NextResponse } from 'next/server';
|
|
10
10
|
import { AnonSessionPreferences } from '../../lib/anon-session';
|
|
11
|
-
/**
|
|
12
|
-
* GET handler - retrieves anonymous session preferences
|
|
13
|
-
*/
|
|
14
|
-
export declare function GET(request: NextRequest): Promise<NextResponse<{
|
|
15
|
-
success: boolean;
|
|
16
|
-
data: {
|
|
17
|
-
id: string;
|
|
18
|
-
preferences: AnonSessionPreferences;
|
|
19
|
-
metrics: import("../../lib/anon-session").AnonSessionMetrics;
|
|
20
|
-
};
|
|
21
|
-
}> | NextResponse<{
|
|
22
|
-
success: boolean;
|
|
23
|
-
error: string;
|
|
24
|
-
}>>;
|
|
25
11
|
/**
|
|
26
12
|
* POST handler - updates anonymous session preferences
|
|
27
13
|
*/
|
|
14
|
+
export declare const GET: (request: NextRequest) => Promise<Response>;
|
|
28
15
|
export declare function POST(request: NextRequest): Promise<NextResponse<{
|
|
29
16
|
success: boolean;
|
|
30
17
|
error: string;
|
|
@@ -8,17 +8,16 @@
|
|
|
8
8
|
* Sets a cookie to track anonymous visitors and stores preferences in Redis.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.GET =
|
|
11
|
+
exports.GET = void 0;
|
|
12
12
|
exports.POST = POST;
|
|
13
13
|
const server_1 = require("next/server");
|
|
14
14
|
const headers_1 = require("next/headers");
|
|
15
15
|
const anon_session_1 = require("../../lib/anon-session");
|
|
16
16
|
const log_safe_1 = require("../../lib/log-safe");
|
|
17
|
+
const no_store_1 = require("../../lib/no-store");
|
|
17
18
|
const COOKIE_MAX_AGE = 90 * 24 * 60 * 60; // 90 days in seconds
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
*/
|
|
21
|
-
async function GET(request) {
|
|
19
|
+
/** Retrieves anonymous session preferences. Exported as GET below, wrapped in noStore (PAY-2994). */
|
|
20
|
+
async function getPreferences(request) {
|
|
22
21
|
try {
|
|
23
22
|
const cookieStore = await (0, headers_1.cookies)();
|
|
24
23
|
let anonId = cookieStore.get(anon_session_1.ANON_COOKIE_NAME)?.value;
|
|
@@ -53,6 +52,7 @@ async function GET(request) {
|
|
|
53
52
|
/**
|
|
54
53
|
* POST handler - updates anonymous session preferences
|
|
55
54
|
*/
|
|
55
|
+
exports.GET = (0, no_store_1.noStore)(getPreferences);
|
|
56
56
|
async function POST(request) {
|
|
57
57
|
try {
|
|
58
58
|
const cookieStore = await (0, headers_1.cookies)();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHealthHandler = createHealthHandler;
|
|
4
|
+
/**
|
|
5
|
+
* /api/health handler (PAY-2804, AUTH-045; BAPert ruling 88065).
|
|
6
|
+
*
|
|
7
|
+
* Sessions are Redis-only. The body reports state ONLY: `redis: 'up' | 'down' | 'unknown'` and
|
|
8
|
+
* `status: 'ok' | 'degraded'`. No REDIS_URL, host, port, error text or stack, to any caller (anonymous route).
|
|
9
|
+
*
|
|
10
|
+
* The HTTP status is 200 even when Redis is down: this route is the ALB target-group check, and a 503
|
|
11
|
+
* there would make the ALB pull every task for an outage that replacing tasks cannot fix.
|
|
12
|
+
* HEALTH_REDIS_FAILS_STATUS=1 turns Redis-down into 503 as a READINESS setting for a deployer's OWN probe;
|
|
13
|
+
* it is default off and must never be set on a load-balancer health check.
|
|
14
|
+
*/
|
|
15
|
+
const server_1 = require("next/server");
|
|
16
|
+
const PING_BUDGET_MS = 2000;
|
|
17
|
+
async function probe(redis) {
|
|
18
|
+
let timer;
|
|
19
|
+
try {
|
|
20
|
+
const timeout = new Promise((_, reject) => {
|
|
21
|
+
timer = setTimeout(() => reject(new Error('ping timeout')), PING_BUDGET_MS);
|
|
22
|
+
});
|
|
23
|
+
await Promise.race([redis.ping(), timeout]);
|
|
24
|
+
return 'up';
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return 'down';
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
if (timer)
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function createHealthHandler(getRedis) {
|
|
35
|
+
return async function GET() {
|
|
36
|
+
let redis;
|
|
37
|
+
let client;
|
|
38
|
+
try {
|
|
39
|
+
client = getRedis();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
client = undefined;
|
|
43
|
+
}
|
|
44
|
+
redis = client ? await probe(client) : 'unknown';
|
|
45
|
+
const failsStatus = process.env.HEALTH_REDIS_FAILS_STATUS === '1';
|
|
46
|
+
return server_1.NextResponse.json({ status: redis === 'up' ? 'ok' : 'degraded', redis }, { status: redis !== 'up' && failsStatus ? 503 : 200, headers: { 'Cache-Control': 'no-store' } });
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
import { NextRequest
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
reason: string;
|
|
5
|
-
error: string;
|
|
6
|
-
code: string;
|
|
7
|
-
}> | NextResponse<{
|
|
8
|
-
canRefresh: boolean;
|
|
9
|
-
reason: string;
|
|
10
|
-
sessionTokenId: string;
|
|
11
|
-
}> | NextResponse<{
|
|
12
|
-
canRefresh: boolean;
|
|
13
|
-
reason: string;
|
|
14
|
-
sessionTokenId: null;
|
|
15
|
-
}>>;
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
/** PAY-2994 (NightHawk 92964 Low 1): the api-handlers/session/* path is also in the exports map; wrap here too so a consumer importing it is covered. */
|
|
3
|
+
export declare const GET: (req: NextRequest) => Promise<Response>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GET =
|
|
3
|
+
exports.GET = void 0;
|
|
4
4
|
const server_1 = require("next/server");
|
|
5
5
|
const session_store_1 = require("../../lib/session-store");
|
|
6
6
|
const refresh_token_validator_1 = require("../../lib/refresh-token-validator");
|
|
@@ -8,8 +8,9 @@ const test_aware_get_token_1 = require("../../lib/test-aware-get-token");
|
|
|
8
8
|
const logger_1 = require("../../config/logger");
|
|
9
9
|
const token_log_id_1 = require("../../lib/token-log-id");
|
|
10
10
|
const log_safe_1 = require("../../lib/log-safe");
|
|
11
|
+
const no_store_1 = require("../../lib/no-store");
|
|
11
12
|
const auth_1 = require("../../server/auth");
|
|
12
|
-
async function
|
|
13
|
+
async function getRefreshViability(req) {
|
|
13
14
|
try {
|
|
14
15
|
const sessionToken = req.nextUrl.searchParams.get('token');
|
|
15
16
|
let finalSessionToken = sessionToken;
|
|
@@ -59,3 +60,5 @@ async function GET(req) {
|
|
|
59
60
|
return server_1.NextResponse.json({ canRefresh: false, reason: 'check_error', error: 'Failed to check refresh viability', code: 'INTERNAL_ERROR', correlationId }, { status: 500 });
|
|
60
61
|
}
|
|
61
62
|
}
|
|
63
|
+
/** PAY-2994 (NightHawk 92964 Low 1): the api-handlers/session/* path is also in the exports map; wrap here too so a consumer importing it is covered. */
|
|
64
|
+
exports.GET = (0, no_store_1.noStore)(getRefreshViability);
|
|
@@ -3,11 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This API route is called by the middleware to securely check if a session is valid.
|
|
5
5
|
*/
|
|
6
|
-
import { NextRequest
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
message: string;
|
|
10
|
-
code: string;
|
|
11
|
-
}> | NextResponse<{
|
|
12
|
-
authenticated: boolean;
|
|
13
|
-
}>>;
|
|
6
|
+
import { NextRequest } from 'next/server';
|
|
7
|
+
/** PAY-2994 (NightHawk 92964 Low 1): the api-handlers/session/* path is also in the exports map; wrap here too so a consumer importing it is covered. */
|
|
8
|
+
export declare const GET: (req: NextRequest) => Promise<Response>;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* This API route is called by the middleware to securely check if a session is valid.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.GET =
|
|
8
|
+
exports.GET = void 0;
|
|
9
9
|
const revocation_fields_1 = require("../../auth/revocation-fields");
|
|
10
10
|
const server_1 = require("next/server");
|
|
11
11
|
const session_store_1 = require("../../lib/session-store");
|
|
@@ -15,7 +15,8 @@ const idp_client_config_1 = require("../../lib/idp-client-config");
|
|
|
15
15
|
const token_log_id_1 = require("../../lib/token-log-id");
|
|
16
16
|
const sessions_1 = require("../../vibe/sessions");
|
|
17
17
|
const log_safe_1 = require("../../lib/log-safe");
|
|
18
|
-
|
|
18
|
+
const no_store_1 = require("../../lib/no-store");
|
|
19
|
+
async function getViability(req) {
|
|
19
20
|
try {
|
|
20
21
|
// Ensure initialization is complete (auth signing secret resolved from IDP)
|
|
21
22
|
if (!process.env.BETTER_AUTH_SECRET && !process.env.NEXTAUTH_SECRET) {
|
|
@@ -222,3 +223,5 @@ async function GET(req) {
|
|
|
222
223
|
return server_1.NextResponse.json({ authenticated: false }, { status: 500 });
|
|
223
224
|
}
|
|
224
225
|
}
|
|
226
|
+
/** PAY-2994 (NightHawk 92964 Low 1): the api-handlers/session/* path is also in the exports map; wrap here too so a consumer importing it is covered. */
|
|
227
|
+
exports.GET = (0, no_store_1.noStore)(getViability);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const MAGIC_LINK_ADVISORY = "GHSA-965c-763c-88jm";
|
|
2
|
+
export declare const MAGIC_LINK_FIX_VERSION = "1.7.7";
|
|
3
|
+
/** The installed better-auth version, read from its own package.json (the package does not export './package.json'), or null when it cannot be found. */
|
|
4
|
+
export declare function installedBetterAuthVersion(): string | null;
|
|
5
|
+
/** True when `version` is strictly below `floor` (numeric major.minor.patch; a pre-release suffix is ignored). An unparseable version is NOT proven fixed, so it counts as below. */
|
|
6
|
+
export declare function versionBelow(version: string | null, floor: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Warn when the magic-link plugin is enabled on a better-auth below the fix. Returns whether it warned (so a test can assert it). `warn` and `version` are
|
|
9
|
+
* injectable for the same reason.
|
|
10
|
+
*/
|
|
11
|
+
export declare function warnIfMagicLinkBelowFix(magicLinkEnabled: boolean, warn?: (msg: string) => void, version?: string | null): boolean;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAGIC_LINK_FIX_VERSION = exports.MAGIC_LINK_ADVISORY = void 0;
|
|
4
|
+
exports.installedBetterAuthVersion = installedBetterAuthVersion;
|
|
5
|
+
exports.versionBelow = versionBelow;
|
|
6
|
+
exports.warnIfMagicLinkBelowFix = warnIfMagicLinkBelowFix;
|
|
7
|
+
/**
|
|
8
|
+
* PAY-2592 slice 1: the exposure guard for better-auth GHSA-965c-763c-88jm (critical, CVSS 9.1; affected >=1.4.0-beta.18 <1.7.7).
|
|
9
|
+
*
|
|
10
|
+
* "OAuth state can be used as a Magic Link to sign in as another user." It needs ALL of: the Magic Link plugin enabled, an OAuth provider, and OAuth state
|
|
11
|
+
* stored with `account.storeStateStrategy: "database"` (better-auth's default when a database OR secondaryStorage exists). This package has secondaryStorage,
|
|
12
|
+
* so what keeps it unexposed is, today:
|
|
13
|
+
* 1. `account.storeStateStrategy: 'cookie'` is pinned in better-auth.ts (the advisory: cookie-backed state blocks the path), and
|
|
14
|
+
* 2. the magic-link plugin registers ONLY when a consumer passes `opts.magicLink`.
|
|
15
|
+
* Nothing in this package fixes the flaw; the fix is better-auth >= 1.7.7 (PAY-2592, spike pending). Until then a consumer that turns the plugin on is told, loudly,
|
|
16
|
+
* once per process.
|
|
17
|
+
*
|
|
18
|
+
* test/next-mvp-pay-2592-exposure-pins.test.cjs holds both conditions and this warning.
|
|
19
|
+
*/
|
|
20
|
+
const node_fs_1 = require("node:fs");
|
|
21
|
+
const node_path_1 = require("node:path");
|
|
22
|
+
exports.MAGIC_LINK_ADVISORY = 'GHSA-965c-763c-88jm';
|
|
23
|
+
exports.MAGIC_LINK_FIX_VERSION = '1.7.7';
|
|
24
|
+
/** The installed better-auth version, read from its own package.json (the package does not export './package.json'), or null when it cannot be found. */
|
|
25
|
+
function installedBetterAuthVersion() {
|
|
26
|
+
try {
|
|
27
|
+
let dir = (0, node_path_1.dirname)(require.resolve('better-auth'));
|
|
28
|
+
for (let i = 0; i < 8; i++) {
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, 'package.json'), 'utf8'));
|
|
31
|
+
if (pkg.name === 'better-auth' && typeof pkg.version === 'string')
|
|
32
|
+
return pkg.version;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* no package.json at this level */
|
|
36
|
+
}
|
|
37
|
+
const up = (0, node_path_1.dirname)(dir);
|
|
38
|
+
if (up === dir)
|
|
39
|
+
break;
|
|
40
|
+
dir = up;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* unresolvable */
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/** True when `version` is strictly below `floor` (numeric major.minor.patch; a pre-release suffix is ignored). An unparseable version is NOT proven fixed, so it counts as below. */
|
|
49
|
+
function versionBelow(version, floor) {
|
|
50
|
+
const parse = (v) => /^(\d+)\.(\d+)\.(\d+)/.exec(v)?.slice(1, 4).map(Number) ?? null;
|
|
51
|
+
const a = version ? parse(version) : null;
|
|
52
|
+
const b = parse(floor);
|
|
53
|
+
if (!a || !b)
|
|
54
|
+
return true;
|
|
55
|
+
for (let i = 0; i < 3; i++) {
|
|
56
|
+
if (a[i] !== b[i])
|
|
57
|
+
return a[i] < b[i];
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Warn when the magic-link plugin is enabled on a better-auth below the fix. Returns whether it warned (so a test can assert it). `warn` and `version` are
|
|
63
|
+
* injectable for the same reason.
|
|
64
|
+
*/
|
|
65
|
+
function warnIfMagicLinkBelowFix(magicLinkEnabled, warn = (m) => console.warn(m), version = installedBetterAuthVersion()) {
|
|
66
|
+
if (!magicLinkEnabled)
|
|
67
|
+
return false;
|
|
68
|
+
if (!versionBelow(version, exports.MAGIC_LINK_FIX_VERSION))
|
|
69
|
+
return false;
|
|
70
|
+
warn(`[BETTER_AUTH] WARNING ${exports.MAGIC_LINK_ADVISORY} (critical): the magic-link plugin is enabled on better-auth ${version ?? '(version unknown)'}, below the fix ${exports.MAGIC_LINK_FIX_VERSION}. ` +
|
|
71
|
+
`This package keeps account.storeStateStrategy='cookie', which blocks the attack; do NOT change that while magic link is on, and upgrade better-auth (PAY-2592).`);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
package/dist/auth/better-auth.js
CHANGED
|
@@ -78,6 +78,10 @@ const single_flight_1 = require("../lib/single-flight");
|
|
|
78
78
|
const session_store_1 = require("../lib/session-store");
|
|
79
79
|
const token_log_id_1 = require("../lib/token-log-id");
|
|
80
80
|
const log_safe_1 = require("../lib/log-safe");
|
|
81
|
+
const better_auth_exposure_1 = require("./better-auth-exposure");
|
|
82
|
+
const no_store_1 = require("../lib/no-store");
|
|
83
|
+
/** PAY-2592: the magic-link advisory warning fires once per process. */
|
|
84
|
+
let magicLinkAdvisoryWarned = false;
|
|
81
85
|
/**
|
|
82
86
|
* 185121: short sha256 fingerprint for secrets/tokens in log lines — enough
|
|
83
87
|
* to tell "same" from "divergent", never enough to reconstruct. 12 hex chars
|
|
@@ -489,6 +493,12 @@ function buildBetterAuthProviders(config) {
|
|
|
489
493
|
function createBetterAuthInstance(idpConfig, opts = {}) {
|
|
490
494
|
(0, app_slug_1.assertAppSlugMatchesIdp)(idpConfig.clientSlug);
|
|
491
495
|
const appSlug = idpConfig.clientSlug || (0, app_slug_1.getAppSlug)();
|
|
496
|
+
// PAY-2592 (GHSA-965c-763c-88jm): a consumer that enables the magic-link plugin on a better-auth below 1.7.7 is told, once per process. What keeps the
|
|
497
|
+
// package unexposed is account.storeStateStrategy='cookie' below plus the plugin being opt-in; see better-auth-exposure.ts.
|
|
498
|
+
if (opts.magicLink && !magicLinkAdvisoryWarned) {
|
|
499
|
+
magicLinkAdvisoryWarned = true;
|
|
500
|
+
(0, better_auth_exposure_1.warnIfMagicLinkBelowFix)(true);
|
|
501
|
+
}
|
|
492
502
|
// 185121 line 1: name the secret this instance PINNED, as a hash, never the
|
|
493
503
|
// secret. 183047's surviving fork is HMAC-fail vs record-miss; an HMAC-fail
|
|
494
504
|
// caused by a divergent secret is only diagnosable if every instance
|
|
@@ -1410,7 +1420,7 @@ function authErrorCodeFromLocation(location) {
|
|
|
1410
1420
|
* ```
|
|
1411
1421
|
*/
|
|
1412
1422
|
function createAuthGetHandler(loginPath = '/account-auth/login') {
|
|
1413
|
-
|
|
1423
|
+
const get = async function GET(request) {
|
|
1414
1424
|
// Card 138635: establish the request-scoped carrier for the provider's raw
|
|
1415
1425
|
// id_token. It must wrap BOTH ba.GET (which invokes getUserInfo, the only
|
|
1416
1426
|
// hook that sees the token) and the exchange below (which forwards it).
|
|
@@ -1523,4 +1533,6 @@ function createAuthGetHandler(loginPath = '/account-auth/login') {
|
|
|
1523
1533
|
return response;
|
|
1524
1534
|
});
|
|
1525
1535
|
};
|
|
1536
|
+
// PAY-2994: every Better Auth GET answer (session, OAuth callbacks, redirects) is per-user and may set cookies; never cached.
|
|
1537
|
+
return (0, no_store_1.noStore)(get);
|
|
1526
1538
|
}
|
|
@@ -75,6 +75,15 @@ const defaultUnauthenticatedRoutes = [
|
|
|
75
75
|
allowDuringCircuitBreakerOpen: true,
|
|
76
76
|
requiresRateLimit: false
|
|
77
77
|
},
|
|
78
|
+
{
|
|
79
|
+
// PAY-2804: the exact path. '/api/health/*' matches startsWith('/api/health/'), which the bare
|
|
80
|
+
// '/api/health' (the ALB check) does not, so without this entry it sat behind sign-in.
|
|
81
|
+
pattern: '/api/health',
|
|
82
|
+
description: 'Health check endpoint (ALB target; anonymous, state only)',
|
|
83
|
+
allowDuringCircuitBreakerOpen: true,
|
|
84
|
+
// An ALB check comes from few IPs: a rate limit here would answer 429 and get tasks pulled (NightHawk 89179).
|
|
85
|
+
requiresRateLimit: false
|
|
86
|
+
},
|
|
78
87
|
{
|
|
79
88
|
pattern: '/api/health/*',
|
|
80
89
|
description: 'Health check endpoints',
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-Control: no-store for GET routes whose answer carries a person's, a tenant's or a session's data (PAY-2994).
|
|
3
|
+
*
|
|
4
|
+
* WHY: the portal's BFF no-store ratchet (testing/qa/bff-no-store-ratchet.js) found 8 GET routes re-exported from this package that sent no
|
|
5
|
+
* Cache-Control. Their answers carry per-user data and some carry tokens, so a browser or an intermediate cache must never keep them.
|
|
6
|
+
*
|
|
7
|
+
* `noStore(handler)` wraps a route handler and sets `Cache-Control: no-store` on EVERY response it returns (errors and redirects too: nothing
|
|
8
|
+
* a session route says is worth caching). It REPLACES an existing Cache-Control, never merges, so a looser value cannot survive. A Response
|
|
9
|
+
* whose headers are immutable (a fetch() result, Response.redirect()) is rebuilt with the same status, body and headers (Set-Cookie kept).
|
|
10
|
+
*/
|
|
11
|
+
export declare const NO_STORE_VALUE = "no-store";
|
|
12
|
+
export declare function withNoStoreHeader(res: Response): Response;
|
|
13
|
+
export declare function noStore<A extends unknown[], R extends Response>(handler: (...args: A) => R | Promise<R>): (...args: A) => Promise<Response>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NO_STORE_VALUE = void 0;
|
|
4
|
+
exports.withNoStoreHeader = withNoStoreHeader;
|
|
5
|
+
exports.noStore = noStore;
|
|
6
|
+
/**
|
|
7
|
+
* Cache-Control: no-store for GET routes whose answer carries a person's, a tenant's or a session's data (PAY-2994).
|
|
8
|
+
*
|
|
9
|
+
* WHY: the portal's BFF no-store ratchet (testing/qa/bff-no-store-ratchet.js) found 8 GET routes re-exported from this package that sent no
|
|
10
|
+
* Cache-Control. Their answers carry per-user data and some carry tokens, so a browser or an intermediate cache must never keep them.
|
|
11
|
+
*
|
|
12
|
+
* `noStore(handler)` wraps a route handler and sets `Cache-Control: no-store` on EVERY response it returns (errors and redirects too: nothing
|
|
13
|
+
* a session route says is worth caching). It REPLACES an existing Cache-Control, never merges, so a looser value cannot survive. A Response
|
|
14
|
+
* whose headers are immutable (a fetch() result, Response.redirect()) is rebuilt with the same status, body and headers (Set-Cookie kept).
|
|
15
|
+
*/
|
|
16
|
+
exports.NO_STORE_VALUE = 'no-store';
|
|
17
|
+
function withNoStoreHeader(res) {
|
|
18
|
+
// Not a Response (no headers object): hand it back untouched. A Next route always returns a Response, so this only keeps a hand-made
|
|
19
|
+
// test double from being turned into an empty Response.
|
|
20
|
+
if (!res || typeof res !== 'object' || !res.headers)
|
|
21
|
+
return res;
|
|
22
|
+
try {
|
|
23
|
+
res.headers.set('Cache-Control', exports.NO_STORE_VALUE);
|
|
24
|
+
return res;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Immutable headers (TypeError). Rebuild: same status, statusText, body and every header; Headers copies Set-Cookie entries intact.
|
|
28
|
+
const headers = new Headers(res.headers);
|
|
29
|
+
headers.set('Cache-Control', exports.NO_STORE_VALUE);
|
|
30
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function noStore(handler) {
|
|
34
|
+
return async (...args) => withNoStoreHeader(await handler(...args));
|
|
35
|
+
}
|
|
@@ -13,15 +13,8 @@
|
|
|
13
13
|
* @since auth-ready-v2
|
|
14
14
|
*/
|
|
15
15
|
import { NextRequest, NextResponse } from 'next/server';
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
* Returns the current session information including:
|
|
20
|
-
* - User details
|
|
21
|
-
* - Token expiry status
|
|
22
|
-
* - Session validity
|
|
23
|
-
*/
|
|
24
|
-
export declare function GET(req: NextRequest): Promise<NextResponse<{}>>;
|
|
16
|
+
/** PAY-2994: session state is per-user and some answers carry tokens; never cached (noStore). */
|
|
17
|
+
export declare const GET: (req: NextRequest) => Promise<Response>;
|
|
25
18
|
/**
|
|
26
19
|
* POST /api/auth/session - Update session METADATA
|
|
27
20
|
*
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* @since auth-ready-v2
|
|
15
15
|
*/
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.GET =
|
|
17
|
+
exports.GET = void 0;
|
|
18
18
|
exports.POST = POST;
|
|
19
|
+
const no_store_1 = require("../../lib/no-store");
|
|
19
20
|
const server_1 = require("next/server");
|
|
20
21
|
const auth_1 = require("../../server/auth");
|
|
21
22
|
const session_store_1 = require("../../lib/session-store");
|
|
@@ -29,7 +30,7 @@ const token_log_id_1 = require("../../lib/token-log-id");
|
|
|
29
30
|
* - Token expiry status
|
|
30
31
|
* - Session validity
|
|
31
32
|
*/
|
|
32
|
-
async function
|
|
33
|
+
async function getHandler(req) {
|
|
33
34
|
try {
|
|
34
35
|
const authSession = await (0, auth_1.getSession)(req);
|
|
35
36
|
if (!authSession) {
|
|
@@ -116,6 +117,8 @@ async function GET(req) {
|
|
|
116
117
|
}, { status: 500 });
|
|
117
118
|
}
|
|
118
119
|
}
|
|
120
|
+
/** PAY-2994: session state is per-user and some answers carry tokens; never cached (noStore). */
|
|
121
|
+
exports.GET = (0, no_store_1.noStore)(getHandler);
|
|
119
122
|
/**
|
|
120
123
|
* The fields this route USED to accept from the request body and must never accept
|
|
121
124
|
* again. Cards 308833 (critical) / 312092 (remediation spec) / 318272 / 311425.
|
|
@@ -13,57 +13,6 @@
|
|
|
13
13
|
* @version 2.0.0
|
|
14
14
|
* @since auth-ready-v2
|
|
15
15
|
*/
|
|
16
|
-
import { NextRequest
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* GET /api/session/viability - Check if session is viable
|
|
20
|
-
*
|
|
21
|
-
* Returns:
|
|
22
|
-
* - viable: boolean - Whether the session can be used
|
|
23
|
-
* - needsRefresh: boolean - Whether a refresh is recommended
|
|
24
|
-
* - expiresIn: number - Seconds until token expires
|
|
25
|
-
*/
|
|
26
|
-
export declare function GET(req: NextRequest): Promise<NextResponse<{
|
|
27
|
-
viable: boolean;
|
|
28
|
-
needsRefresh: boolean;
|
|
29
|
-
authenticated: boolean;
|
|
30
|
-
revocationStatus: RevocationStatus;
|
|
31
|
-
unavailable: boolean;
|
|
32
|
-
error: string;
|
|
33
|
-
code: string;
|
|
34
|
-
}> | NextResponse<{
|
|
35
|
-
viable: boolean;
|
|
36
|
-
needsRefresh: boolean;
|
|
37
|
-
authenticated: boolean;
|
|
38
|
-
reason: string;
|
|
39
|
-
}> | NextResponse<{
|
|
40
|
-
tenantRequiresTwoFactor: boolean;
|
|
41
|
-
userHasCompletedTenantTwoFactorRequirements: boolean;
|
|
42
|
-
userStillNeedsTwoFactor: boolean;
|
|
43
|
-
tenantAllowed2FAMethods: string[] | null;
|
|
44
|
-
nextStep: "enroll_mfa" | "mfa_verify" | null | undefined;
|
|
45
|
-
requires2FA: boolean;
|
|
46
|
-
twoFactorComplete: boolean;
|
|
47
|
-
accessTokenExpired: boolean;
|
|
48
|
-
expiresAt: string;
|
|
49
|
-
roles: string[];
|
|
50
|
-
clientId: string | null;
|
|
51
|
-
forceInvalid: boolean;
|
|
52
|
-
unavailable: boolean;
|
|
53
|
-
revoked: boolean;
|
|
54
|
-
viable: boolean;
|
|
55
|
-
needsRefresh: boolean;
|
|
56
|
-
expiresIn: number;
|
|
57
|
-
hasRefreshToken: boolean;
|
|
58
|
-
authenticated: boolean;
|
|
59
|
-
sessionTokenId: string | null;
|
|
60
|
-
revocationStatus: RevocationStatus;
|
|
61
|
-
}> | NextResponse<{
|
|
62
|
-
viable: boolean;
|
|
63
|
-
needsRefresh: boolean;
|
|
64
|
-
authenticated: boolean;
|
|
65
|
-
revocationStatus: RevocationStatus;
|
|
66
|
-
error: string;
|
|
67
|
-
code: string;
|
|
68
|
-
correlationId: string;
|
|
69
|
-
}>>;
|
|
16
|
+
import { NextRequest } from 'next/server';
|
|
17
|
+
/** PAY-2994: session state is per-user and some answers carry tokens; never cached (noStore). */
|
|
18
|
+
export declare const GET: (req: NextRequest) => Promise<Response>;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* @since auth-ready-v2
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.GET =
|
|
18
|
+
exports.GET = void 0;
|
|
19
19
|
const revocation_fields_1 = require("../../auth/revocation-fields");
|
|
20
20
|
const server_1 = require("next/server");
|
|
21
21
|
const auth_1 = require("../../server/auth");
|
|
@@ -23,6 +23,7 @@ const session_store_1 = require("../../lib/session-store");
|
|
|
23
23
|
const idp_client_config_1 = require("../../lib/idp-client-config");
|
|
24
24
|
const safe_error_response_1 = require("../../lib/safe-error-response");
|
|
25
25
|
const token_log_id_1 = require("../../lib/token-log-id");
|
|
26
|
+
const no_store_1 = require("../../lib/no-store");
|
|
26
27
|
const sessions_1 = require("../../vibe/sessions");
|
|
27
28
|
/**
|
|
28
29
|
* Get tenant-wide 2FA requirement from cached client config (from broker handshake)
|
|
@@ -135,7 +136,7 @@ function resolveClientId(sessionClientId) {
|
|
|
135
136
|
* - needsRefresh: boolean - Whether a refresh is recommended
|
|
136
137
|
* - expiresIn: number - Seconds until token expires
|
|
137
138
|
*/
|
|
138
|
-
async function
|
|
139
|
+
async function getHandler(req) {
|
|
139
140
|
try {
|
|
140
141
|
// PAY-2216: ask for the session's STATE, not a session-or-null. A null used to cover a store outage, a deleted
|
|
141
142
|
// session and no cookie alike, so this route answered 'No session found' to all three and the middleware could
|
|
@@ -495,3 +496,5 @@ async function GET(req) {
|
|
|
495
496
|
}, { status: 500 });
|
|
496
497
|
}
|
|
497
498
|
}
|
|
499
|
+
/** PAY-2994: session state is per-user and some answers carry tokens; never cached (noStore). */
|
|
500
|
+
exports.GET = (0, no_store_1.noStore)(getHandler);
|