@payez/next-mvp 4.12.0 → 4.12.2

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/api-handlers/account/profile.d.ts +2 -39
  3. package/dist/api-handlers/account/profile.js +5 -2
  4. package/dist/api-handlers/anon/preferences.d.ts +2 -12
  5. package/dist/api-handlers/anon/preferences.js +8 -5
  6. package/dist/api-handlers/session/refresh-viability.d.ts +3 -15
  7. package/dist/api-handlers/session/refresh-viability.js +5 -2
  8. package/dist/api-handlers/session/viability.d.ts +3 -8
  9. package/dist/api-handlers/session/viability.js +5 -2
  10. package/dist/auth/better-auth.js +4 -1
  11. package/dist/lib/ensure-fresh-access-token.js +15 -10
  12. package/dist/lib/no-store.d.ts +13 -0
  13. package/dist/lib/no-store.js +35 -0
  14. package/dist/routes/auth/session.d.ts +2 -9
  15. package/dist/routes/auth/session.js +5 -2
  16. package/dist/routes/auth/viability.d.ts +3 -54
  17. package/dist/routes/auth/viability.js +5 -2
  18. package/dist/routes/mfa/methods.d.ts +2 -2
  19. package/dist/routes/mfa/methods.js +3 -1
  20. package/dist/routes/passkeys/list.d.ts +2 -10
  21. package/dist/routes/passkeys/list.js +4 -2
  22. package/dist/routes/session/refresh-viability.d.ts +2 -1
  23. package/dist/routes/session/refresh-viability.js +4 -2
  24. package/package.json +1 -1
  25. package/src/api-handlers/account/profile.ts +5 -1
  26. package/src/api-handlers/anon/preferences.ts +8 -4
  27. package/src/api-handlers/session/refresh-viability.ts +5 -1
  28. package/src/api-handlers/session/viability.ts +6 -2
  29. package/src/auth/better-auth.ts +4 -1
  30. package/src/lib/ensure-fresh-access-token.ts +15 -10
  31. package/src/lib/no-store.ts +30 -0
  32. package/src/routes/auth/session.ts +5 -1
  33. package/src/routes/auth/viability.ts +6 -2
  34. package/src/routes/mfa/methods.ts +3 -1
  35. package/src/routes/passkeys/list.ts +5 -1
  36. package/src/routes/session/refresh-viability.ts +5 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog — @payez/next-mvp
2
2
 
3
+ ## 4.12.2 (BACKPORT: to be published under dist-tag `backport` only, never `next` or `latest`; = 4.12.1 + the PAY-2994 no-store commit and nothing else)
4
+
5
+ A patch cut from the 4.12.1 tree (DevOps commit 603bbcd2, whose src equals the published 4.12.1 tarball file for file). Carries ONE change, so the portal can take the no-store fix without 4.13.0's behaviour changes. 4.13.1 carries the same fix on the 4.13.x line.
6
+
7
+ - **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, `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.
8
+ - **NOT covered (follow-up):** 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`.
9
+
10
+ ## 4.12.1 (to be published under dist-tag `next` only, after 4.12.0; `latest` stays 4.8.9 until Jon says otherwise)
11
+
12
+ A patch cut from the 4.12.0 tree carrying ONE change: the PAY-2425 hygiene fix below (PR 1500).
13
+
14
+ - **CHANGE (PAY-2425, hygiene): the refresh no longer invents authentication claims for an OAuth (Google) session.** `lib/ensure-fresh-access-token` answered an OAuth session whose stored authentication methods were empty with `amr ['pwd','mfa']`, `two_factor_method 'oauth'` and an `acr` raised from '1' to '2' ("OAuth IS a form of multi-factor auth"), and echoed a stored `mfaVerified` as `two_factor_verified` whatever the session's methods said. OAuth is federated sign-in, not a second factor (BAPert 77404). The refresh body now carries only what the session records: empty stays empty, `acr` is the session's own level, no method is named from silence, and `two_factor_verified` is sent only when the session's recorded methods contain `mfa`, `sms`, `totp` or `email`. A session that really attests `mfa` still sends it. The IdP ignores these request fields and mints `amr`/`acr` from the stored refresh row, so this removes a false claim from the wire; it does not establish that any bypass existed or is closed. **What changes for a caller:** a Google user on a `require2FA` client is asked for a code, as before, because the IdP decides that; a Google-only client that wants no second factor sets `require2FA = false` explicitly. An earlier version of this fix on `nextpert/375639-amr-fabrication` (e6e3b6c3, e3b3bb12) was never merged; the refresh path has since moved into this helper. Rows: `test/next-mvp-pay-2425-no-fabricated-mfa.test.cjs` (R1/R2/R3, C1, N1 and four mutants).
15
+
3
16
  ## 4.12.0 (to be published under dist-tag `next` only, after 4.11.0; `latest` stays 4.8.9 until Jon says otherwise)
4
17
 
5
18
  A minor: PII-in-logs groups 1, 2, 3a and 3b, the GOV-023 signout BREAKING change, the logout-401 FIX (PAY-2396), and the BREAKING site-logger signatures (3b: callers of `siteEvents.loginFailed` / `siteEvents.error` must change).
@@ -1,41 +1,4 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- /**
3
- * Account Profile API Handler
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 = 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 GET(req) {
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 {
@@ -9,19 +9,9 @@
9
9
  import { NextRequest, NextResponse } from 'next/server';
10
10
  import { AnonSessionPreferences } from '../../lib/anon-session';
11
11
  /**
12
- * GET handler - retrieves anonymous session preferences
12
+ * POST handler - updates anonymous session preferences
13
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
- }>>;
14
+ export declare const GET: (request: NextRequest) => Promise<Response>;
25
15
  /**
26
16
  * POST handler - updates anonymous session preferences
27
17
  */
@@ -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 = 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
- * GET handler - retrieves anonymous session preferences
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;
@@ -50,6 +49,10 @@ async function GET(request) {
50
49
  return server_1.NextResponse.json({ success: false, error: 'Failed to get preferences' }, { status: 500 });
51
50
  }
52
51
  }
52
+ /**
53
+ * POST handler - updates anonymous session preferences
54
+ */
55
+ exports.GET = (0, no_store_1.noStore)(getPreferences);
53
56
  /**
54
57
  * POST handler - updates anonymous session preferences
55
58
  */
@@ -1,15 +1,3 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- export declare function GET(req: NextRequest): Promise<NextResponse<{
3
- canRefresh: boolean;
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 = 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 GET(req) {
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, NextResponse } from 'next/server';
7
- export declare function GET(req: NextRequest): Promise<NextResponse<{
8
- error: string;
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 = 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
- async function GET(req) {
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) {
@@ -218,3 +219,5 @@ async function GET(req) {
218
219
  return server_1.NextResponse.json({ authenticated: false }, { status: 500 });
219
220
  }
220
221
  }
222
+ /** 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. */
223
+ exports.GET = (0, no_store_1.noStore)(getViability);
@@ -77,6 +77,7 @@ const single_flight_1 = require("../lib/single-flight");
77
77
  const session_store_1 = require("../lib/session-store");
78
78
  const token_log_id_1 = require("../lib/token-log-id");
79
79
  const log_safe_1 = require("../lib/log-safe");
80
+ const no_store_1 = require("../lib/no-store");
80
81
  /**
81
82
  * 185121: short sha256 fingerprint for secrets/tokens in log lines — enough
82
83
  * to tell "same" from "divergent", never enough to reconstruct. 12 hex chars
@@ -1395,7 +1396,7 @@ async function abortOAuthSignIn(callbackResponse, sessionToken, cookieHeader, lo
1395
1396
  * ```
1396
1397
  */
1397
1398
  function createAuthGetHandler(loginPath = '/account-auth/login') {
1398
- return async function GET(request) {
1399
+ const get = async function GET(request) {
1399
1400
  // Card 138635: establish the request-scoped carrier for the provider's raw
1400
1401
  // id_token. It must wrap BOTH ba.GET (which invokes getUserInfo, the only
1401
1402
  // hook that sees the token) and the exchange below (which forwards it).
@@ -1496,4 +1497,6 @@ function createAuthGetHandler(loginPath = '/account-auth/login') {
1496
1497
  return response;
1497
1498
  });
1498
1499
  };
1500
+ // PAY-2994: every Better Auth GET answer (session, OAuth callbacks, redirects) is per-user and may set cookies; never cached.
1501
+ return (0, no_store_1.noStore)(get);
1499
1502
  }
@@ -257,17 +257,18 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
257
257
  }
258
258
  catch { /* fall through */ }
259
259
  }
260
- const isOAuthSession = !!session.oauthProvider;
261
- if (authMethods.length === 0 && isOAuthSession) {
262
- authMethods = ['pwd', 'mfa'];
263
- }
260
+ // PAY-2425 (BAPert 77400, NightHawk 77401): THE CLIENT DOES NOT INFER AUTHENTICATION STRENGTH FROM SILENCE. This block used to answer an OAuth
261
+ // (Google) session whose stored methods were EMPTY with amr ['pwd','mfa'], a two-factor method of 'oauth' and an acr raised from '1' to '2' ("OAuth IS
262
+ // a form of multi-factor auth"). That is false: OAuth is federated sign-in, one factor typed at the provider. It fired exactly when the session
263
+ // could tell us nothing and answered with the strongest claim available; on prod (4.10.0) a Google user on a require2FA client was then counted
264
+ // 2FA-complete with no code (RigPert 77398). What the session does not attest, the refresh body does not assert: empty stays empty, the level stays
265
+ // the session's own, and no method is named unless one is recorded. The IdP computes the real amr/acr for the minted token (and mfaVerified is
266
+ // re-derived from THAT token below); a session that really attests 'mfa' still sends it. History: the fix existed on nextpert/375639-amr-fabrication
267
+ // (e6e3b6c3, e3b3bb12) and was never merged.
264
268
  const twoFactorMethod = authMethods.find(m => ['sms', 'totp', 'email'].includes(m)) ||
265
269
  session.mfaMethod ||
266
- (isOAuthSession ? 'oauth' : null);
267
- let acrValue = String(session.authenticationLevel ?? '1');
268
- if (isOAuthSession && session.mfaVerified && acrValue === '1') {
269
- acrValue = '2';
270
- }
270
+ null;
271
+ const acrValue = String(session.authenticationLevel ?? '1');
271
272
  // WO-2: send the POST-lock current refresh token, not the pre-lock snapshot.
272
273
  // `session` was read before we acquired the lock; a concurrent refresh that
273
274
  // completed while we waited rotates the token, so the pre-lock value is stale
@@ -300,7 +301,11 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
300
301
  amr: authMethods,
301
302
  acr: acrValue,
302
303
  };
303
- if (session.mfaVerified)
304
+ // PAY-2425 (NightHawk 77414): a STORED mfaVerified flag is only echoed when the session's own recorded methods back it. Sessions created while the
305
+ // bug was live carry mfaVerified:true that the bug itself wrote, with no 'mfa' (or sms/totp/email) in their methods; echoing that flag would carry the
306
+ // poison across the upgrade. A genuine second factor leaves 'mfa' and its method in the recorded methods, so it is still sent.
307
+ const methodsAttestSecondFactor = authMethods.some(m => m === 'mfa' || ['sms', 'totp', 'email'].includes(m));
308
+ if (session.mfaVerified && methodsAttestSecondFactor)
304
309
  body.two_factor_verified = true;
305
310
  if (twoFactorMethod)
306
311
  body.two_factor_method = twoFactorMethod;
@@ -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
- * GET /api/auth/session - Check current session status
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 = 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 GET(req) {
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, NextResponse } from 'next/server';
17
- import { type RevocationStatus } from '../../vibe/sessions';
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 = 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 GET(req) {
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
@@ -486,3 +487,5 @@ async function GET(req) {
486
487
  }, { status: 500 });
487
488
  }
488
489
  }
490
+ /** PAY-2994: session state is per-user and some answers carry tokens; never cached (noStore). */
491
+ exports.GET = (0, no_store_1.noStore)(getHandler);
@@ -10,8 +10,8 @@
10
10
  * ```
11
11
  */
12
12
  import { NextRequest } from 'next/server';
13
- import { GET as listMethods } from '../../api-handlers/mfa/methods';
14
- export declare const GET: typeof listMethods;
13
+ /** PAY-2994: the user's MFA methods; never cached (noStore). */
14
+ export declare const GET: (req: NextRequest) => Promise<Response>;
15
15
  type Ctx = {
16
16
  params: Promise<{
17
17
  id: string;
@@ -3,7 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GET = void 0;
4
4
  exports.DELETE = DELETE;
5
5
  const methods_1 = require("../../api-handlers/mfa/methods");
6
- exports.GET = methods_1.GET;
6
+ const no_store_1 = require("../../lib/no-store");
7
+ /** PAY-2994: the user's MFA methods; never cached (noStore). */
8
+ exports.GET = (0, no_store_1.noStore)(methods_1.GET);
7
9
  async function DELETE(req, ctx) {
8
10
  const { id } = await ctx.params;
9
11
  return (0, methods_1.DELETE)(req, id);
@@ -1,10 +1,2 @@
1
- /**
2
- * Ready-to-Use Passkey List Route
3
- *
4
- * @example
5
- * ```typescript
6
- * // app/api/passkeys/route.ts
7
- * export { GET } from '@payez/next-mvp/routes/passkeys/list';
8
- * ```
9
- */
10
- export { GET } from '../../api-handlers/passkeys/list';
1
+ /** PAY-2994: the signed-in user's passkeys; never cached (noStore). */
2
+ export declare const GET: (req: import("next/server").NextRequest) => Promise<Response>;
@@ -10,5 +10,7 @@ exports.GET = void 0;
10
10
  * export { GET } from '@payez/next-mvp/routes/passkeys/list';
11
11
  * ```
12
12
  */
13
- var list_1 = require("../../api-handlers/passkeys/list");
14
- Object.defineProperty(exports, "GET", { enumerable: true, get: function () { return list_1.GET; } });
13
+ const list_1 = require("../../api-handlers/passkeys/list");
14
+ const no_store_1 = require("../../lib/no-store");
15
+ /** PAY-2994: the signed-in user's passkeys; never cached (noStore). */
16
+ exports.GET = (0, no_store_1.noStore)(list_1.GET);
@@ -13,4 +13,5 @@
13
13
  * @version 2.0.0
14
14
  * @since auth-ready-v2
15
15
  */
16
- export { GET } from '../../api-handlers/session/refresh-viability';
16
+ /** PAY-2994: refresh-token viability of this session; never cached (noStore). */
17
+ export declare const GET: (req: import("next/server").NextRequest) => Promise<Response>;
@@ -16,5 +16,7 @@
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.GET = void 0;
19
- var refresh_viability_1 = require("../../api-handlers/session/refresh-viability");
20
- Object.defineProperty(exports, "GET", { enumerable: true, get: function () { return refresh_viability_1.GET; } });
19
+ const refresh_viability_1 = require("../../api-handlers/session/refresh-viability");
20
+ const no_store_1 = require("../../lib/no-store");
21
+ /** PAY-2994: refresh-token viability of this session; never cached (noStore). */
22
+ exports.GET = (0, no_store_1.noStore)(refresh_viability_1.GET);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payez/next-mvp",
3
- "version": "4.12.0",
3
+ "version": "4.12.2",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,6 +1,7 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { idpFetchJSON } from '../../lib/idp-fetch';
3
3
  import { ENV_CONFIG } from '../../config/env';
4
+ import { noStore } from '../../lib/no-store';
4
5
 
5
6
  /**
6
7
  * Account Profile API Handler
@@ -41,7 +42,7 @@ import { ENV_CONFIG } from '../../config/env';
41
42
  * @package @payez/next-mvp
42
43
  */
43
44
 
44
- export async function GET(req: NextRequest) {
45
+ async function getProfile(req: NextRequest) {
45
46
  const result = await idpFetchJSON(req, `${ENV_CONFIG.IDP_URL}/api/Account/profile`, {
46
47
  method: 'GET',
47
48
  headers: { 'Content-Type': 'application/json' },
@@ -65,6 +66,9 @@ export async function GET(req: NextRequest) {
65
66
  return NextResponse.json(result.json, { status: 200 });
66
67
  }
67
68
 
69
+ /** PAY-2994: the profile is one person's data; never cached (noStore). */
70
+ export const GET = noStore(getProfile);
71
+
68
72
  export async function PUT(req: NextRequest) {
69
73
  let body = '{}';
70
74
  try {
@@ -16,13 +16,12 @@ import {
16
16
  AnonSessionPreferences,
17
17
  } from '../../lib/anon-session';
18
18
  import { errForLog } from '../../lib/log-safe';
19
+ import { noStore } from '../../lib/no-store';
19
20
 
20
21
  const COOKIE_MAX_AGE = 90 * 24 * 60 * 60; // 90 days in seconds
21
22
 
22
- /**
23
- * GET handler - retrieves anonymous session preferences
24
- */
25
- export async function GET(request: NextRequest) {
23
+ /** Retrieves anonymous session preferences. Exported as GET below, wrapped in noStore (PAY-2994). */
24
+ async function getPreferences(request: NextRequest) {
26
25
  try {
27
26
  const cookieStore = await cookies();
28
27
  let anonId = cookieStore.get(ANON_COOKIE_NAME)?.value;
@@ -61,6 +60,11 @@ export async function GET(request: NextRequest) {
61
60
  }
62
61
  }
63
62
 
63
+ /**
64
+ * POST handler - updates anonymous session preferences
65
+ */
66
+ export const GET = noStore(getPreferences);
67
+
64
68
  /**
65
69
  * POST handler - updates anonymous session preferences
66
70
  */
@@ -5,9 +5,10 @@ import { getTokenTestAware } from '../../lib/test-aware-get-token';
5
5
  import { logger } from '../../config/logger';
6
6
  import { tokenLogId } from '../../lib/token-log-id';
7
7
  import { errForLog } from '../../lib/log-safe';
8
+ import { noStore } from '../../lib/no-store';
8
9
  import { resolveSessionState } from '../../server/auth';
9
10
 
10
- export async function GET(req: NextRequest) {
11
+ async function getRefreshViability(req: NextRequest) {
11
12
  try {
12
13
  const sessionToken = req.nextUrl.searchParams.get('token');
13
14
  let finalSessionToken = sessionToken;
@@ -55,3 +56,6 @@ export async function GET(req: NextRequest) {
55
56
  return NextResponse.json({ canRefresh: false, reason: 'check_error', error: 'Failed to check refresh viability', code: 'INTERNAL_ERROR', correlationId }, { status: 500 });
56
57
  }
57
58
  }
59
+
60
+ /** 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. */
61
+ export const GET = noStore(getRefreshViability);
@@ -13,8 +13,9 @@ import { getIDPClientConfig } from '../../lib/idp-client-config';
13
13
  import { tokenLogId } from '../../lib/token-log-id';
14
14
  import { getRevocationStatus, warnNonNumericUserId, resolveNumericIdpUserId, type RevocationStatus } from '../../vibe/sessions';
15
15
  import { errForLog } from '../../lib/log-safe';
16
+ import { noStore } from '../../lib/no-store';
16
17
 
17
- export async function GET(req: NextRequest) {
18
+ async function getViability(req: NextRequest) {
18
19
  try {
19
20
  // Ensure initialization is complete (auth signing secret resolved from IDP)
20
21
  if (!process.env.BETTER_AUTH_SECRET && !process.env.NEXTAUTH_SECRET) {
@@ -237,4 +238,7 @@ export async function GET(req: NextRequest) {
237
238
  console.error('[API Viability] Error checking session viability:', errForLog(error));
238
239
  return NextResponse.json({ authenticated: false }, { status: 500 });
239
240
  }
240
- }
241
+ }
242
+
243
+ /** 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. */
244
+ export const GET = noStore(getViability);
@@ -34,6 +34,7 @@ import { createSingleFlight, type SingleFlight } from '../lib/single-flight';
34
34
  import { SESSION_TTL } from '../lib/session-store';
35
35
  import { tokenLogId } from '../lib/token-log-id';
36
36
  import { errForLog } from '../lib/log-safe';
37
+ import { noStore } from '../lib/no-store';
37
38
 
38
39
  /**
39
40
  * 185121: short sha256 fingerprint for secrets/tokens in log lines — enough
@@ -1582,7 +1583,7 @@ async function abortOAuthSignIn(
1582
1583
  * ```
1583
1584
  */
1584
1585
  export function createAuthGetHandler(loginPath: string = '/account-auth/login') {
1585
- return async function GET(request: Request): Promise<Response> {
1586
+ const get = async function GET(request: Request): Promise<Response> {
1586
1587
  // Card 138635: establish the request-scoped carrier for the provider's raw
1587
1588
  // id_token. It must wrap BOTH ba.GET (which invokes getUserInfo, the only
1588
1589
  // hook that sees the token) and the exchange below (which forwards it).
@@ -1686,4 +1687,6 @@ export function createAuthGetHandler(loginPath: string = '/account-auth/login')
1686
1687
  return response;
1687
1688
  });
1688
1689
  };
1690
+ // PAY-2994: every Better Auth GET answer (session, OAuth callbacks, redirects) is per-user and may set cookies; never cached.
1691
+ return noStore(get);
1689
1692
  }
@@ -349,19 +349,20 @@ export async function ensureFreshAccessToken(
349
349
  } else if (typeof session.authenticationMethods === 'string') {
350
350
  try { authMethods = JSON.parse(session.authenticationMethods); } catch { /* fall through */ }
351
351
  }
352
- const isOAuthSession = !!session.oauthProvider;
353
- if (authMethods.length === 0 && isOAuthSession) {
354
- authMethods = ['pwd', 'mfa'];
355
- }
352
+ // PAY-2425 (BAPert 77400, NightHawk 77401): THE CLIENT DOES NOT INFER AUTHENTICATION STRENGTH FROM SILENCE. This block used to answer an OAuth
353
+ // (Google) session whose stored methods were EMPTY with amr ['pwd','mfa'], a two-factor method of 'oauth' and an acr raised from '1' to '2' ("OAuth IS
354
+ // a form of multi-factor auth"). That is false: OAuth is federated sign-in, one factor typed at the provider. It fired exactly when the session
355
+ // could tell us nothing and answered with the strongest claim available; on prod (4.10.0) a Google user on a require2FA client was then counted
356
+ // 2FA-complete with no code (RigPert 77398). What the session does not attest, the refresh body does not assert: empty stays empty, the level stays
357
+ // the session's own, and no method is named unless one is recorded. The IdP computes the real amr/acr for the minted token (and mfaVerified is
358
+ // re-derived from THAT token below); a session that really attests 'mfa' still sends it. History: the fix existed on nextpert/375639-amr-fabrication
359
+ // (e6e3b6c3, e3b3bb12) and was never merged.
356
360
  const twoFactorMethod =
357
361
  authMethods.find(m => ['sms', 'totp', 'email'].includes(m)) ||
358
362
  session.mfaMethod ||
359
- (isOAuthSession ? 'oauth' : null);
363
+ null;
360
364
 
361
- let acrValue = String(session.authenticationLevel ?? '1');
362
- if (isOAuthSession && session.mfaVerified && acrValue === '1') {
363
- acrValue = '2';
364
- }
365
+ const acrValue = String(session.authenticationLevel ?? '1');
365
366
 
366
367
  // WO-2: send the POST-lock current refresh token, not the pre-lock snapshot.
367
368
  // `session` was read before we acquired the lock; a concurrent refresh that
@@ -397,7 +398,11 @@ export async function ensureFreshAccessToken(
397
398
  amr: authMethods,
398
399
  acr: acrValue,
399
400
  };
400
- if (session.mfaVerified) body.two_factor_verified = true;
401
+ // PAY-2425 (NightHawk 77414): a STORED mfaVerified flag is only echoed when the session's own recorded methods back it. Sessions created while the
402
+ // bug was live carry mfaVerified:true that the bug itself wrote, with no 'mfa' (or sms/totp/email) in their methods; echoing that flag would carry the
403
+ // poison across the upgrade. A genuine second factor leaves 'mfa' and its method in the recorded methods, so it is still sent.
404
+ const methodsAttestSecondFactor = authMethods.some(m => m === 'mfa' || ['sms', 'totp', 'email'].includes(m));
405
+ if (session.mfaVerified && methodsAttestSecondFactor) body.two_factor_verified = true;
401
406
  if (twoFactorMethod) body.two_factor_method = twoFactorMethod;
402
407
  if (session.mfaCompletedAt) body.two_factor_completed_at = new Date(session.mfaCompletedAt).toISOString();
403
408
 
@@ -0,0 +1,30 @@
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 const NO_STORE_VALUE = 'no-store';
12
+
13
+ export function withNoStoreHeader(res: Response): Response {
14
+ // Not a Response (no headers object): hand it back untouched. A Next route always returns a Response, so this only keeps a hand-made
15
+ // test double from being turned into an empty Response.
16
+ if (!res || typeof res !== 'object' || !(res as { headers?: unknown }).headers) return res;
17
+ try {
18
+ res.headers.set('Cache-Control', NO_STORE_VALUE);
19
+ return res;
20
+ } catch {
21
+ // Immutable headers (TypeError). Rebuild: same status, statusText, body and every header; Headers copies Set-Cookie entries intact.
22
+ const headers = new Headers(res.headers);
23
+ headers.set('Cache-Control', NO_STORE_VALUE);
24
+ return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
25
+ }
26
+ }
27
+
28
+ export function noStore<A extends unknown[], R extends Response>(handler: (...args: A) => R | Promise<R>): (...args: A) => Promise<Response> {
29
+ return async (...args: A) => withNoStoreHeader(await handler(...args));
30
+ }
@@ -13,6 +13,7 @@
13
13
  * @since auth-ready-v2
14
14
  */
15
15
 
16
+ import { noStore } from '../../lib/no-store';
16
17
  import { NextRequest, NextResponse } from 'next/server';
17
18
  import { getSession as getBetterAuthSession } from '../../server/auth';
18
19
  import { getSession as getRedisSession, updateSession } from '../../lib/session-store';
@@ -27,7 +28,7 @@ import { tokenLogId } from '../../lib/token-log-id';
27
28
  * - Token expiry status
28
29
  * - Session validity
29
30
  */
30
- export async function GET(req: NextRequest) {
31
+ async function getHandler(req: NextRequest) {
31
32
  try {
32
33
  const authSession = await getBetterAuthSession(req);
33
34
 
@@ -121,6 +122,9 @@ export async function GET(req: NextRequest) {
121
122
  }
122
123
  }
123
124
 
125
+ /** PAY-2994: session state is per-user and some answers carry tokens; never cached (noStore). */
126
+ export const GET = noStore(getHandler);
127
+
124
128
  /**
125
129
  * The fields this route USED to accept from the request body and must never accept
126
130
  * again. Cards 308833 (critical) / 312092 (remediation spec) / 318272 / 311425.
@@ -24,6 +24,7 @@ import {
24
24
  import { getIDPClientConfig } from '../../lib/idp-client-config';
25
25
  import { logExceptionWithCorrelation } from '../../lib/safe-error-response';
26
26
  import { tokenLogId } from '../../lib/token-log-id';
27
+ import { noStore } from '../../lib/no-store';
27
28
  import { getRevocationStatus, warnNonNumericUserId, resolveNumericIdpUserId, type RevocationStatus } from '../../vibe/sessions';
28
29
 
29
30
  /**
@@ -141,7 +142,7 @@ function resolveClientId(sessionClientId: string | undefined): string | null {
141
142
  * - needsRefresh: boolean - Whether a refresh is recommended
142
143
  * - expiresIn: number - Seconds until token expires
143
144
  */
144
- export async function GET(req: NextRequest) {
145
+ async function getHandler(req: NextRequest) {
145
146
  try {
146
147
  // PAY-2216: ask for the session's STATE, not a session-or-null. A null used to cover a store outage, a deleted
147
148
  // session and no cookie alike, so this route answered 'No session found' to all three and the middleware could
@@ -517,4 +518,7 @@ export async function GET(req: NextRequest) {
517
518
  correlationId
518
519
  }, { status: 500 });
519
520
  }
520
- }
521
+ }
522
+
523
+ /** PAY-2994: session state is per-user and some answers carry tokens; never cached (noStore). */
524
+ export const GET = noStore(getHandler);
@@ -11,8 +11,10 @@
11
11
  */
12
12
  import { NextRequest } from 'next/server';
13
13
  import { GET as listMethods, DELETE as deleteMethod } from '../../api-handlers/mfa/methods';
14
+ import { noStore } from '../../lib/no-store';
14
15
 
15
- export const GET = listMethods;
16
+ /** PAY-2994: the user's MFA methods; never cached (noStore). */
17
+ export const GET = noStore(listMethods);
16
18
 
17
19
  type Ctx = { params: Promise<{ id: string }> };
18
20
 
@@ -7,4 +7,8 @@
7
7
  * export { GET } from '@payez/next-mvp/routes/passkeys/list';
8
8
  * ```
9
9
  */
10
- export { GET } from '../../api-handlers/passkeys/list';
10
+ import { GET as listPasskeys } from '../../api-handlers/passkeys/list';
11
+ import { noStore } from '../../lib/no-store';
12
+
13
+ /** PAY-2994: the signed-in user's passkeys; never cached (noStore). */
14
+ export const GET = noStore(listPasskeys);
@@ -14,4 +14,8 @@
14
14
  * @since auth-ready-v2
15
15
  */
16
16
 
17
- export { GET } from '../../api-handlers/session/refresh-viability';
17
+ import { GET as refreshViability } from '../../api-handlers/session/refresh-viability';
18
+ import { noStore } from '../../lib/no-store';
19
+
20
+ /** PAY-2994: refresh-token viability of this session; never cached (noStore). */
21
+ export const GET = noStore(refreshViability);