@payez/next-mvp 4.9.0 → 4.11.0

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 (50) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/dist/api-handlers/auth/refresh.d.ts +17 -56
  3. package/dist/api-handlers/auth/refresh.js +71 -696
  4. package/dist/auth/utils/token-utils.d.ts +1 -1
  5. package/dist/auth/utils/token-utils.js +3 -2
  6. package/dist/lib/api-handler.js +4 -3
  7. package/dist/lib/ensure-fresh-access-token.d.ts +24 -1
  8. package/dist/lib/ensure-fresh-access-token.js +255 -29
  9. package/dist/lib/idp-fetch.js +3 -2
  10. package/dist/lib/idp-logout.d.ts +4 -2
  11. package/dist/lib/refresh-in-doubt.d.ts +31 -0
  12. package/dist/lib/refresh-in-doubt.js +81 -0
  13. package/dist/lib/refresh-rotation.d.ts +19 -0
  14. package/dist/lib/refresh-rotation.js +18 -0
  15. package/dist/lib/refresh-token-validator.js +2 -1
  16. package/dist/lib/refresh-window.d.ts +16 -0
  17. package/dist/lib/refresh-window.js +19 -0
  18. package/dist/lib/safe-trace-id.d.ts +11 -0
  19. package/dist/lib/safe-trace-id.js +23 -0
  20. package/dist/lib/session-store.d.ts +0 -5
  21. package/dist/lib/session-store.js +28 -1
  22. package/dist/lib/token-lifecycle.js +3 -2
  23. package/dist/models/SessionModel.d.ts +7 -0
  24. package/dist/routes/auth/refresh.d.ts +1 -14
  25. package/dist/server/auth.d.ts +1 -1
  26. package/dist/stores/authStore.js +3 -2
  27. package/package.json +3 -12
  28. package/scripts/verify-release-provenance.mjs +1 -0
  29. package/src/api-handlers/auth/refresh.ts +72 -736
  30. package/src/auth/utils/token-utils.ts +3 -2
  31. package/src/lib/api-handler.ts +4 -3
  32. package/src/lib/ensure-fresh-access-token.ts +275 -30
  33. package/src/lib/idp-fetch.ts +3 -2
  34. package/src/lib/idp-logout.ts +4 -2
  35. package/src/lib/refresh-in-doubt.ts +79 -0
  36. package/src/lib/refresh-rotation.ts +18 -0
  37. package/src/lib/refresh-token-validator.ts +2 -1
  38. package/src/lib/refresh-window.ts +16 -0
  39. package/src/lib/safe-trace-id.ts +21 -0
  40. package/src/lib/session-store.ts +31 -1
  41. package/src/lib/token-lifecycle.ts +3 -2
  42. package/src/models/SessionModel.ts +7 -0
  43. package/src/server/auth.ts +1 -1
  44. package/src/stores/authStore.ts +3 -2
  45. package/dist/api/auth-handler.d.ts +0 -64
  46. package/dist/api/auth-handler.js +0 -402
  47. package/dist/api/index.d.ts +0 -10
  48. package/dist/api/index.js +0 -19
  49. package/src/api/auth-handler.ts +0 -551
  50. package/src/api/index.ts +0 -18
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REQUEST_NEVER_LEFT_CODES = exports.UPSTREAM_TIMEOUT_CODE = exports.REFRESH_OUTCOME_UNKNOWN_CODE = exports.REFRESH_FETCH_TIMEOUT_MS = void 0;
4
+ exports.refreshTokenIdentity = refreshTokenIdentity;
5
+ exports.inDoubtPatch = inDoubtPatch;
6
+ exports.isRefreshInDoubt = isRefreshInDoubt;
7
+ exports.isTimeoutError = isTimeoutError;
8
+ exports.requestNeverLeft = requestNeverLeft;
9
+ /**
10
+ * The IdP fetch timeout and the "refresh in doubt" marker (BAPert 76397 decision 3, corrected by 76403; Core facts DotNetPert 76400).
11
+ *
12
+ * THE PROBLEM. Core consumes a refresh token with one atomic UPDATE and has NO reuse grace: re-presenting a used token revokes every newer
13
+ * refresh token the USER holds, on every device (RevokeDescendantTokens). So when OUR fetch to the IdP times out, we do not know whether Core
14
+ * consumed the token and minted a replacement whose answer we never saw. The store still holds the token we SENT (nothing was persisted).
15
+ * Holding the refresh lock only delays the next request by its 60 s TTL; after that the next request re-reads the same token and replays it.
16
+ *
17
+ * THE RULE. A timed-out refresh answers 504 UPSTREAM_TIMEOUT and records, on the session row, an IDENTITY (a SHA-256, never the token and
18
+ * never a prefix of it) of the token that was sent. Any later attempt whose token-to-send matches that identity refuses WITHOUT calling the
19
+ * IdP: terminal REFRESH_OUTCOME_UNKNOWN / REAUTHENTICATE. One login on one session is better than a user-wide revocation. The marker is
20
+ * dropped by the same write that stores a DIFFERENT refresh token (session-store `dropStaleDiscardMarker`); nothing tries to resolve the doubt
21
+ * without a login.
22
+ *
23
+ * Leaf module: no imports from the rest of the package, so session-store can use it without a cycle.
24
+ */
25
+ const crypto_1 = require("crypto");
26
+ /** How long a refresh fetch (connect, response and body) may take. 5 s lock wait + this + the persist stays well inside the 60 s lock TTL. */
27
+ exports.REFRESH_FETCH_TIMEOUT_MS = 10_000;
28
+ /** The typed terminal answer for an attempt that would re-send an in-doubt token. */
29
+ exports.REFRESH_OUTCOME_UNKNOWN_CODE = 'REFRESH_OUTCOME_UNKNOWN';
30
+ /** The typed answer for the timed-out call itself. */
31
+ exports.UPSTREAM_TIMEOUT_CODE = 'UPSTREAM_TIMEOUT';
32
+ /** A one-way identity for a refresh token. Not reversible and not a prefix. */
33
+ function refreshTokenIdentity(token) {
34
+ return (0, crypto_1.createHash)('sha256').update(token, 'utf8').digest('hex');
35
+ }
36
+ /** The row fields recorded at a timeout. */
37
+ function inDoubtPatch(sentToken) {
38
+ return { refreshInDoubtTokenHash: refreshTokenIdentity(sentToken), refreshInDoubtAt: Date.now() };
39
+ }
40
+ /** True when the token we are about to send is the one a previous attempt left in doubt. */
41
+ function isRefreshInDoubt(session, tokenToSend) {
42
+ if (!session || typeof session.refreshInDoubtTokenHash !== 'string' || !session.refreshInDoubtTokenHash)
43
+ return false;
44
+ if (typeof tokenToSend !== 'string' || tokenToSend.length === 0)
45
+ return false;
46
+ return refreshTokenIdentity(tokenToSend) === session.refreshInDoubtTokenHash;
47
+ }
48
+ /** True for the error `AbortSignal.timeout` produces (a DOMException named TimeoutError); AbortError is treated the same. */
49
+ function isTimeoutError(err) {
50
+ const name = err?.name;
51
+ return name === 'TimeoutError' || name === 'AbortError';
52
+ }
53
+ /**
54
+ * The ONLY transport error codes that PROVE the request never left this process (BAPert 76433 item 4): the connection was refused, or the
55
+ * name did not resolve. Anything else (ECONNRESET, UND_ERR_SOCKET, a timeout, an error with no code) may have happened AFTER the request was
56
+ * written, so the IdP may have consumed the token: outcome unknown. The cost is lopsided on purpose: a false in-doubt costs one re-login, a
57
+ * missed one costs a user-wide revocation. Do NOT add a code here without proof it can only occur before the request is sent.
58
+ */
59
+ exports.REQUEST_NEVER_LEFT_CODES = new Set(['ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN']);
60
+ /** Every `code` on the error, its `cause`, and the members of an AggregateError (undici wraps the socket error in `cause`). */
61
+ function transportCodes(err, depth = 0, out = []) {
62
+ if (!err || typeof err !== 'object' || depth > 4)
63
+ return out;
64
+ const e = err;
65
+ if (typeof e.code === 'string' && e.code)
66
+ out.push(e.code);
67
+ if (e.cause)
68
+ transportCodes(e.cause, depth + 1, out);
69
+ if (Array.isArray(e.errors))
70
+ for (const inner of e.errors)
71
+ transportCodes(inner, depth + 1, out);
72
+ return out;
73
+ }
74
+ /**
75
+ * True only when the error carries at least one cause code and EVERY code it carries is in the never-left allowlist. An error with no code
76
+ * is NOT proof, so it is in-doubt.
77
+ */
78
+ function requestNeverLeft(err) {
79
+ const codes = transportCodes(err);
80
+ return codes.length > 0 && codes.every((c) => exports.REQUEST_NEVER_LEFT_CODES.has(c));
81
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 4.9.0 (BAPert 76359): ONE predicate for "a concurrent refresh already rotated the refresh token, and the row it left is usable", shared by
3
+ * all four places that decide it (api-handlers/auth/refresh.ts and lib/ensure-fresh-access-token.ts, each for the HTTP-401 branch and the
4
+ * 200 success:false branch). Before, two said "usable" meant PRESENT (an access token and an expiry exist) and two said UNEXPIRED, so a rotated
5
+ * row whose access token had already expired was answered as a successful refresh by some branches and not by others.
6
+ *
7
+ * Usable = an access token exists AND its recorded expiry is in the future. A row that is rotated but NOT usable is answered with the
8
+ * retryable 502, its (rotated) refresh token KEPT and NO discard marker, in every branch. The CAS that decides "rotated" (a different
9
+ * stored refresh token than the one we sent) is unchanged: a different token is never discarded.
10
+ *
11
+ * A leaf module: no imports.
12
+ */
13
+ export declare function rotatedRowIsUsable<T extends {
14
+ idpAccessToken?: string | null;
15
+ idpAccessTokenExpires?: number | null;
16
+ }>(session: T | null | undefined, now?: number): session is T & {
17
+ idpAccessToken: string;
18
+ idpAccessTokenExpires: number;
19
+ };
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rotatedRowIsUsable = rotatedRowIsUsable;
4
+ /**
5
+ * 4.9.0 (BAPert 76359): ONE predicate for "a concurrent refresh already rotated the refresh token, and the row it left is usable", shared by
6
+ * all four places that decide it (api-handlers/auth/refresh.ts and lib/ensure-fresh-access-token.ts, each for the HTTP-401 branch and the
7
+ * 200 success:false branch). Before, two said "usable" meant PRESENT (an access token and an expiry exist) and two said UNEXPIRED, so a rotated
8
+ * row whose access token had already expired was answered as a successful refresh by some branches and not by others.
9
+ *
10
+ * Usable = an access token exists AND its recorded expiry is in the future. A row that is rotated but NOT usable is answered with the
11
+ * retryable 502, its (rotated) refresh token KEPT and NO discard marker, in every branch. The CAS that decides "rotated" (a different
12
+ * stored refresh token than the one we sent) is unchanged: a different token is never discarded.
13
+ *
14
+ * A leaf module: no imports.
15
+ */
16
+ function rotatedRowIsUsable(session, now = Date.now()) {
17
+ return !!(session && session.idpAccessToken && session.idpAccessTokenExpires && session.idpAccessTokenExpires > now);
18
+ }
@@ -7,6 +7,7 @@ exports.getRefreshTokenTimeRemaining = getRefreshTokenTimeRemaining;
7
7
  exports.checkRefreshViability = checkRefreshViability;
8
8
  const jwt_decode_1 = require("./jwt-decode");
9
9
  const logger_1 = require("../config/logger");
10
+ const refresh_window_1 = require("./refresh-window");
10
11
  function isRefreshTokenValid(token) {
11
12
  if (!token)
12
13
  return false;
@@ -81,7 +82,7 @@ function checkRefreshViability(sessionData) {
81
82
  if (expiresAtMs < 1000000000000)
82
83
  expiresAtMs = expiresAtMs * 1000;
83
84
  accessTokenTimeRemaining = Math.floor((expiresAtMs - now) / 1000);
84
- const bufferSec = 5 * 60; // 5 minutes pre-expiry buffer
85
+ const bufferSec = refresh_window_1.REFRESH_WINDOW_MS / 1000; // the one refresh window (lib/refresh-window), in seconds
85
86
  accessTokenExpired = accessTokenTimeRemaining <= bufferSec;
86
87
  logger_1.tokenRefreshLogger.debug('[REFRESH_VIABILITY] Access token expiration check', { now, expiresAtMs, accessTokenTimeRemaining, bufferSec, accessTokenExpired });
87
88
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * THE freshness window for an IdP access token (BAPert 76397 decision 1; Core facts DotNetPert 76400).
3
+ *
4
+ * An access token with this much life left or less is treated as stale and refreshed; with more, it is served as it is. One value for
5
+ * every path that decides: the mounted refresh route (`api-handlers/auth/refresh`), `ensureFreshAccessToken`, and `lib/idp-fetch`'s own
6
+ * pre-check. They used to disagree (5 min, 60 s, 5 min), so the same session was "stale" to one path and "fresh" to another, which is how
7
+ * two callers each rotated a token the other had just rotated.
8
+ *
9
+ * WHY 5 MINUTES: Core mints access tokens with a fixed 15-minute lifetime (a code constant, not configuration: Core
10
+ * `TokenConstants.ACCESS_TOKEN_LIFETIME_MINUTES = 15`, read-only in `TokenSettings`). The ruling is 5 minutes when the lifetime is at least 15
11
+ * minutes, and lifetime / 3 otherwise. If Core ever shortens the lifetime, this value MUST be revisited: a window at or above the lifetime
12
+ * makes every request refresh.
13
+ *
14
+ * A caller that needs another value passes it explicitly (`ensureFreshAccessToken`'s `safetyWindowMs` option still exists and still wins).
15
+ */
16
+ export declare const REFRESH_WINDOW_MS: number;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REFRESH_WINDOW_MS = void 0;
4
+ /**
5
+ * THE freshness window for an IdP access token (BAPert 76397 decision 1; Core facts DotNetPert 76400).
6
+ *
7
+ * An access token with this much life left or less is treated as stale and refreshed; with more, it is served as it is. One value for
8
+ * every path that decides: the mounted refresh route (`api-handlers/auth/refresh`), `ensureFreshAccessToken`, and `lib/idp-fetch`'s own
9
+ * pre-check. They used to disagree (5 min, 60 s, 5 min), so the same session was "stale" to one path and "fresh" to another, which is how
10
+ * two callers each rotated a token the other had just rotated.
11
+ *
12
+ * WHY 5 MINUTES: Core mints access tokens with a fixed 15-minute lifetime (a code constant, not configuration: Core
13
+ * `TokenConstants.ACCESS_TOKEN_LIFETIME_MINUTES = 15`, read-only in `TokenSettings`). The ruling is 5 minutes when the lifetime is at least 15
14
+ * minutes, and lifetime / 3 otherwise. If Core ever shortens the lifetime, this value MUST be revisited: a window at or above the lifetime
15
+ * makes every request refresh.
16
+ *
17
+ * A caller that needs another value passes it explicitly (`ensureFreshAccessToken`'s `safetyWindowMs` option still exists and still wins).
18
+ */
19
+ exports.REFRESH_WINDOW_MS = 5 * 60 * 1000;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * A caller-supplied trace id (the `x-request-id` header) is attacker-chosen text, and it goes to the log sink (PAY-2376; BAPert 76321 item 4,
3
+ * NightHawk 76316). Two problems if it is logged as it arrives: a value with a newline forges a log line, and an unbounded value is a log
4
+ * flood. This is the one place that makes it safe to log: control characters are removed, the length is capped, and anything that is not a
5
+ * non-empty string becomes `undefined` (so the log carries no trace id rather than a forged one).
6
+ *
7
+ * It is for LOG CORRELATION ONLY. It is never a lock identity and never a security decision (the lock id is minted server-side).
8
+ */
9
+ /** Longest trace id that is logged. 64 covers a UUID (36) and common trace formats; longer input is cut, not rejected. */
10
+ export declare const MAX_TRACE_ID_LENGTH = 64;
11
+ export declare function sanitizeTraceId(value: unknown): string | undefined;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ /**
3
+ * A caller-supplied trace id (the `x-request-id` header) is attacker-chosen text, and it goes to the log sink (PAY-2376; BAPert 76321 item 4,
4
+ * NightHawk 76316). Two problems if it is logged as it arrives: a value with a newline forges a log line, and an unbounded value is a log
5
+ * flood. This is the one place that makes it safe to log: control characters are removed, the length is capped, and anything that is not a
6
+ * non-empty string becomes `undefined` (so the log carries no trace id rather than a forged one).
7
+ *
8
+ * It is for LOG CORRELATION ONLY. It is never a lock identity and never a security decision (the lock id is minted server-side).
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.MAX_TRACE_ID_LENGTH = void 0;
12
+ exports.sanitizeTraceId = sanitizeTraceId;
13
+ /** Longest trace id that is logged. 64 covers a UUID (36) and common trace formats; longer input is cut, not rejected. */
14
+ exports.MAX_TRACE_ID_LENGTH = 64;
15
+ // C0 controls (incl. CR, LF, TAB, ESC), DEL, C1 controls (incl. NEL U+0085), and the Unicode line/paragraph separators U+2028 / U+2029.
16
+ // eslint-disable-next-line no-control-regex
17
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g;
18
+ function sanitizeTraceId(value) {
19
+ if (typeof value !== 'string')
20
+ return undefined;
21
+ const cleaned = value.replace(CONTROL_CHARS, '').slice(0, exports.MAX_TRACE_ID_LENGTH);
22
+ return cleaned.length > 0 ? cleaned : undefined;
23
+ }
@@ -93,11 +93,6 @@ export declare function deleteSession(sessionToken: string): Promise<void>;
93
93
  * them differently, pass appSlug explicitly.
94
94
  */
95
95
  export declare function deleteSessionEverywhere(sessionToken: string, appSlug?: string): Promise<void>;
96
- /**
97
- * Sets a session directly (for testing or migrations).
98
- * @param sessionToken The session token.
99
- * @param data The session data.
100
- */
101
96
  export declare function setSession(sessionToken: string, data: SessionData): Promise<void>;
102
97
  /**
103
98
  * Updates tokens within an existing session.
@@ -43,6 +43,7 @@ const crypto_1 = require("crypto");
43
43
  const app_slug_1 = require("./app-slug");
44
44
  const token_utils_1 = require("../auth/utils/token-utils");
45
45
  const token_log_id_1 = require("./token-log-id");
46
+ const refresh_in_doubt_1 = require("./refresh-in-doubt");
46
47
  // Use app-slug prefixes for multi-app isolation
47
48
  const getSessionKey = (token) => `${(0, app_slug_1.getSessionPrefix)()}${token}`;
48
49
  const getRefreshLockKey = (token) => `${(0, app_slug_1.getRefreshLockPrefix)()}${token}`;
@@ -271,7 +272,33 @@ async function deleteSessionEverywhere(sessionToken, appSlug) {
271
272
  * @param sessionToken The session token.
272
273
  * @param data The session data.
273
274
  */
275
+ /**
276
+ * 4.9.0 (BAPert 76308 item 2): A LEFTOVER DISCARD MARKER MUST NEVER OUTLIVE A TOKEN. A discard (`refresh.ts`, `ensure-fresh-access-token.ts`)
277
+ * writes `idpRefreshToken: ''` TOGETHER WITH `refreshTokenClearedReason: 'IDP_DISCARD_TOKEN:<code>'`; logout's dead-grant check reads that marker
278
+ * as proof the IdP's grant is gone. If a later write stores a NEW refresh token on the same row (a successful refresh, a re-sign-in into the
279
+ * same row) and the marker stays, a later own-auth 401 would read as "grant already dead" while the grant is live. So any write that leaves a
280
+ * non-empty refresh token on the row drops the marker, in the same write. No code path was found that gives a discarded row a new token
281
+ * today; this is the defence BAPert ruled to keep anyway.
282
+ */
283
+ function dropStaleDiscardMarker(session) {
284
+ let s = session;
285
+ if (typeof s.idpRefreshToken === 'string' && s.idpRefreshToken.length > 0 && (s.refreshTokenClearedReason !== undefined || s.refreshTokenClearedAt !== undefined)) {
286
+ const { refreshTokenClearedReason: _reason, refreshTokenClearedAt: _at, ...rest } = s;
287
+ s = rest;
288
+ }
289
+ // BAPert 76403: the refresh-IN-DOUBT marker (lib/refresh-in-doubt) is cleared by the same write that stores a NEW refresh token, i.e. one
290
+ // whose identity differs from the one left in doubt. A write that stores the SAME token (any caller spreading the whole row back) does
291
+ // NOT clear it: that token is still the in-doubt one.
292
+ if (typeof s.refreshInDoubtTokenHash === 'string' &&
293
+ typeof s.idpRefreshToken === 'string' && s.idpRefreshToken.length > 0 &&
294
+ (0, refresh_in_doubt_1.refreshTokenIdentity)(s.idpRefreshToken) !== s.refreshInDoubtTokenHash) {
295
+ const { refreshInDoubtTokenHash: _hash, refreshInDoubtAt: _doubtAt, ...rest } = s;
296
+ s = rest;
297
+ }
298
+ return s;
299
+ }
274
300
  async function setSession(sessionToken, data) {
301
+ data = dropStaleDiscardMarker(data);
275
302
  const key = getSessionKey(sessionToken);
276
303
  const versionKey = getSessionVersionKey(sessionToken);
277
304
  await redis_1.default.multi()
@@ -405,7 +432,7 @@ async function updateSession(sessionToken, updates) {
405
432
  });
406
433
  }
407
434
  // Merge current session with updates
408
- const updatedSession = { ...currentSession, ...updates };
435
+ const updatedSession = dropStaleDiscardMarker({ ...currentSession, ...updates });
409
436
  // Write the entire updated session back to Redis with version increment
410
437
  await redis_1.default.multi()
411
438
  .setex(key, exports.SESSION_TTL, JSON.stringify(updatedSession))
@@ -28,8 +28,9 @@ const auth_1 = require("../server/auth");
28
28
  const redis_1 = require("./redis");
29
29
  const app_slug_1 = require("./app-slug");
30
30
  const token_log_id_1 = require("./token-log-id");
31
- // 5 minute threshold for "needs refresh" - matches refresh handler pattern
32
- const REFRESH_THRESHOLD_MS = 5 * 60 * 1000;
31
+ const refresh_window_1 = require("./refresh-window");
32
+ // "needs refresh" threshold: THE one freshness window (lib/refresh-window), the same value the refresh route, ensureFreshAccessToken and idp-fetch use.
33
+ const REFRESH_THRESHOLD_MS = refresh_window_1.REFRESH_WINDOW_MS;
33
34
  // Concurrent refresh handling configuration
34
35
  const CONCURRENT_REFRESH_POLL_INTERVAL_MS = 200; // How often to poll session during concurrent refresh
35
36
  const CONCURRENT_REFRESH_MAX_WAIT_MS = 8000; // Max time to wait for concurrent refresh to complete
@@ -38,6 +38,13 @@ export interface SessionData {
38
38
  idpAccessTokenExpires: number;
39
39
  /** When the IDP refresh token expires (Unix timestamp ms) */
40
40
  idpRefreshTokenExpires?: number;
41
+ /**
42
+ * BAPert 76403 (lib/refresh-in-doubt): set when a refresh fetch TIMED OUT. A SHA-256 identity of the refresh token that was sent (never the
43
+ * token), and when. A later attempt whose token-to-send matches refuses without calling the IdP. Cleared by the write that stores a different
44
+ * refresh token.
45
+ */
46
+ refreshInDoubtTokenHash?: string;
47
+ refreshInDoubtAt?: number;
41
48
  /** Decoded IDP access token claims (for quick access without re-decoding) */
42
49
  decodedAccessToken?: any;
43
50
  /**
@@ -14,17 +14,4 @@
14
14
  * @since auth-ready-v2
15
15
  */
16
16
  import { NextRequest } from 'next/server';
17
- export declare function POST(req: NextRequest): Promise<import("next/server").NextResponse<{
18
- error: string;
19
- code: string;
20
- }> | import("next/server").NextResponse<{
21
- error: any;
22
- code: any;
23
- discardToken: boolean;
24
- retryable: boolean;
25
- resolution: any;
26
- }> | import("next/server").NextResponse<{
27
- refreshed: boolean;
28
- accessTokenExpires: number;
29
- hasRefreshToken: boolean;
30
- }>>;
17
+ export declare function POST(req: NextRequest): Promise<import("next/server").NextResponse<unknown>>;
@@ -153,7 +153,7 @@ export interface FreshIdpTokenConfig {
153
153
  idpBaseUrl: string;
154
154
  clientId: string;
155
155
  refreshEndpoint?: string;
156
- /** Refresh if the access token is within this many ms of expiry. Default 60_000. */
156
+ /** Refresh if the access token is within this many ms of expiry. Default REFRESH_WINDOW_MS (5 min, lib/refresh-window). */
157
157
  safetyWindowMs?: number;
158
158
  }
159
159
  /**
@@ -56,6 +56,7 @@ const signalr_1 = require("@microsoft/signalr");
56
56
  const signalrActivityService_1 = require("../services/signalrActivityService");
57
57
  const app_slug_1 = require("../lib/app-slug");
58
58
  const token_log_id_1 = require("../lib/token-log-id");
59
+ const refresh_window_1 = require("../lib/refresh-window");
59
60
  // ===============================
60
61
  // CACHE CONFIGURATION
61
62
  // ===============================
@@ -520,8 +521,8 @@ exports.useAuthStore = (0, zustand_1.create)()((0, middleware_1.devtools)((set,
520
521
  const tokenExpiry = decoded.exp * 1000;
521
522
  const now = Date.now();
522
523
  const timeUntilExpiry = tokenExpiry - now;
523
- // Token should be fresh (not expiring in next 5 minutes)
524
- if (timeUntilExpiry < (5 * 60 * 1000)) {
524
+ // Token should be fresh (not expiring within the one refresh window)
525
+ if (timeUntilExpiry < refresh_window_1.REFRESH_WINDOW_MS) {
525
526
  throw new Error(`Fresh token still expires soon: ${timeUntilExpiry}ms`);
526
527
  }
527
528
  logger_1.authLogger.info('[AuthStore] Session rehydration successful', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payez/next-mvp",
3
- "version": "4.9.0",
3
+ "version": "4.11.0",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,7 +8,8 @@
8
8
  "dist",
9
9
  "scripts",
10
10
  "src",
11
- "POST-INSTALL.md"
11
+ "POST-INSTALL.md",
12
+ "CHANGELOG.md"
12
13
  ],
13
14
  "scripts": {
14
15
  "build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true})\" && npx tsc --project tsconfig.json",
@@ -669,16 +670,6 @@
669
670
  "require": "./dist/stores/authStore.js",
670
671
  "default": "./stores/authStore.js"
671
672
  },
672
- "./api": {
673
- "types": "./dist/api/index.d.ts",
674
- "require": "./dist/api/index.js",
675
- "default": "./dist/api/index.js"
676
- },
677
- "./api/auth-handler": {
678
- "types": "./dist/api/auth-handler.d.ts",
679
- "require": "./dist/api/auth-handler.js",
680
- "default": "./dist/api/auth-handler.js"
681
- },
682
673
  "./routes/auth/refresh": {
683
674
  "types": "./dist/routes/auth/refresh.d.ts",
684
675
  "require": "./dist/routes/auth/refresh.js",
@@ -103,6 +103,7 @@ const CLASSES = [
103
103
  { cls: 'VERIFIED', test: (p) => p.startsWith('src/') },
104
104
  { cls: 'VERIFIED', test: (p) => p.startsWith('scripts/') },
105
105
  { cls: 'VERIFIED', test: (p) => p === 'POST-INSTALL.md' },
106
+ { cls: 'VERIFIED', test: (p) => p === 'CHANGELOG.md' },
106
107
  // npm always ships README regardless of "files"; it is committed, so verify it.
107
108
  { cls: 'VERIFIED', test: (p) => p === 'README.md' },
108
109
  { cls: 'VERIFIED', test: (p) => p === 'LICENSE' || p === 'LICENCE' },