@payez/next-mvp 4.10.0 → 4.12.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.
- package/CHANGELOG.md +44 -0
- package/dist/api-handlers/account/recovery/forward.js +2 -1
- package/dist/api-handlers/account/recovery/initiate.js +2 -1
- package/dist/api-handlers/account/reset-password.js +2 -1
- package/dist/api-handlers/account/send-code.js +2 -1
- package/dist/api-handlers/account/update-phone.js +2 -1
- package/dist/api-handlers/account/validate-password.js +2 -1
- package/dist/api-handlers/account/verify-email.js +2 -1
- package/dist/api-handlers/account/verify-sms.js +2 -1
- package/dist/api-handlers/anon/preferences.js +3 -2
- package/dist/api-handlers/auth/credential-login-email-code.js +3 -2
- package/dist/api-handlers/auth/credential-login.js +6 -5
- package/dist/api-handlers/auth/refresh.d.ts +17 -58
- package/dist/api-handlers/auth/refresh.js +73 -773
- package/dist/api-handlers/auth/signout.js +43 -6
- package/dist/api-handlers/mfa/backup-code-verify.js +2 -1
- package/dist/api-handlers/mfa/totp-verify.js +2 -1
- package/dist/api-handlers/session/refresh-viability.js +3 -2
- package/dist/api-handlers/session/viability.js +2 -1
- package/dist/auth/better-auth.js +7 -6
- package/dist/auth/idp-oauth.js +4 -2
- package/dist/auth/utils/idp-client.js +5 -4
- package/dist/auth/utils/token-utils.d.ts +1 -1
- package/dist/auth/utils/token-utils.js +7 -5
- package/dist/config/logger.js +2 -1
- package/dist/config/logging-config.js +4 -3
- package/dist/config/vibe-log-transport.js +2 -1
- package/dist/lib/api-handler.js +4 -3
- package/dist/lib/ensure-fresh-access-token.d.ts +24 -1
- package/dist/lib/ensure-fresh-access-token.js +195 -28
- package/dist/lib/geolocation.js +5 -22
- package/dist/lib/idp-client-config.js +4 -3
- package/dist/lib/idp-fetch.d.ts +11 -0
- package/dist/lib/idp-fetch.js +6 -3
- package/dist/lib/idp-logout.d.ts +10 -0
- package/dist/lib/idp-logout.js +73 -1
- package/dist/lib/jwt-decode-client.js +2 -1
- package/dist/lib/jwt-decode.js +2 -1
- package/dist/lib/log-safe.d.ts +19 -0
- package/dist/lib/log-safe.js +32 -0
- package/dist/lib/redis.js +2 -1
- package/dist/lib/refresh-in-doubt.d.ts +31 -0
- package/dist/lib/refresh-in-doubt.js +81 -0
- package/dist/lib/refresh-token-validator.js +2 -1
- package/dist/lib/refresh-window.d.ts +16 -0
- package/dist/lib/refresh-window.js +19 -0
- package/dist/lib/safe-error-response.d.ts +5 -1
- package/dist/lib/safe-error-response.js +7 -2
- package/dist/lib/safe-trace-id.d.ts +11 -0
- package/dist/lib/safe-trace-id.js +23 -0
- package/dist/lib/session-store.js +16 -5
- package/dist/lib/site-logger.d.ts +2 -8
- package/dist/lib/site-logger.js +11 -5
- package/dist/lib/standardized-client-api.js +3 -2
- package/dist/lib/token-lifecycle.js +6 -4
- package/dist/middleware/create-middleware.js +2 -1
- package/dist/middleware/rbac-check.d.ts +0 -13
- package/dist/middleware/rbac-check.js +6 -5
- package/dist/models/SessionModel.d.ts +7 -0
- package/dist/routes/auth/logout.js +6 -2
- package/dist/routes/auth/refresh.d.ts +1 -15
- package/dist/routes/auth/settings.js +2 -1
- package/dist/server/auth-guard.js +4 -3
- package/dist/server/auth.d.ts +1 -1
- package/dist/server/auth.js +5 -9
- package/dist/server/decode-session.js +3 -2
- package/dist/server/with-auth.js +2 -1
- package/dist/stores/authStore.js +3 -2
- package/package.json +1 -1
- package/scripts/verify-release-provenance.mjs +1 -0
- package/src/api-handlers/account/recovery/forward.ts +2 -1
- package/src/api-handlers/account/recovery/initiate.ts +2 -1
- package/src/api-handlers/account/reset-password.ts +2 -1
- package/src/api-handlers/account/send-code.ts +2 -1
- package/src/api-handlers/account/update-phone.ts +2 -1
- package/src/api-handlers/account/validate-password.ts +2 -1
- package/src/api-handlers/account/verify-email.ts +2 -1
- package/src/api-handlers/account/verify-sms.ts +2 -1
- package/src/api-handlers/anon/preferences.ts +3 -2
- package/src/api-handlers/auth/credential-login-email-code.ts +3 -2
- package/src/api-handlers/auth/credential-login.ts +6 -5
- package/src/api-handlers/auth/refresh.ts +74 -813
- package/src/api-handlers/auth/signout.ts +54 -7
- package/src/api-handlers/mfa/backup-code-verify.ts +2 -1
- package/src/api-handlers/mfa/totp-verify.ts +2 -1
- package/src/api-handlers/session/refresh-viability.ts +3 -2
- package/src/api-handlers/session/viability.ts +2 -1
- package/src/auth/better-auth.ts +7 -6
- package/src/auth/idp-oauth.ts +7 -2
- package/src/auth/utils/idp-client.ts +5 -4
- package/src/auth/utils/token-utils.ts +7 -5
- package/src/config/logger.ts +2 -1
- package/src/config/logging-config.ts +4 -3
- package/src/config/vibe-log-transport.ts +2 -1
- package/src/lib/api-handler.ts +4 -3
- package/src/lib/ensure-fresh-access-token.ts +214 -29
- package/src/lib/geolocation.ts +5 -4
- package/src/lib/idp-client-config.ts +4 -3
- package/src/lib/idp-fetch.ts +14 -3
- package/src/lib/idp-logout.ts +36 -1
- package/src/lib/jwt-decode-client.ts +3 -1
- package/src/lib/jwt-decode.ts +2 -1
- package/src/lib/log-safe.ts +30 -0
- package/src/lib/redis.ts +2 -1
- package/src/lib/refresh-in-doubt.ts +79 -0
- package/src/lib/refresh-token-validator.ts +2 -1
- package/src/lib/refresh-window.ts +16 -0
- package/src/lib/safe-error-response.ts +7 -2
- package/src/lib/safe-trace-id.ts +21 -0
- package/src/lib/session-store.ts +17 -4
- package/src/lib/site-logger.ts +13 -7
- package/src/lib/standardized-client-api.ts +3 -2
- package/src/lib/token-lifecycle.ts +6 -4
- package/src/middleware/create-middleware.ts +2 -1
- package/src/middleware/rbac-check.ts +2 -1
- package/src/models/SessionModel.ts +7 -0
- package/src/routes/auth/logout.ts +9 -2
- package/src/routes/auth/settings.ts +2 -1
- package/src/server/auth-guard.ts +4 -3
- package/src/server/auth.ts +6 -11
- package/src/server/decode-session.ts +3 -2
- package/src/server/with-auth.ts +2 -1
- package/src/stores/authStore.ts +3 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* log-safe: the ONLY way a caught error or an upstream `code` goes into a server log (PII-in-logs census, BAPert 76553 group 1; ported from the
|
|
3
|
+
* portal's api-error-response.ts, BAPert 76470).
|
|
4
|
+
*
|
|
5
|
+
* Server logs persist and are read beyond the person who made the request. An error's `message`, `stack`, `cause` and `details` can carry an
|
|
6
|
+
* email, a phone number, a code, a token, a SQL fragment or a host: an IdP error's message is the upstream's own free text, and V8's JSON parse
|
|
7
|
+
* error quotes its input. So a log line gets the error's NAME and a closed STATUS / CODE, nothing else. Every field is matched against a closed
|
|
8
|
+
* shape; anything that does not match is dropped, not truncated.
|
|
9
|
+
*
|
|
10
|
+
* No imports on purpose: this is a leaf, loaded by modules that must not pull in anything else.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** An upstream `code` is loggable only if it is a closed token (UPPER_SNAKE). Anything else, including free text, becomes null. */
|
|
14
|
+
export function closedCodeForLog(code: unknown): string | null {
|
|
15
|
+
return typeof code === 'string' && /^[A-Z0-9_]{1,64}$/.test(code) ? code : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Describe a caught error for a log line: `error=<Name> [status=<n>] [code=<CLOSED_CODE>]`.
|
|
20
|
+
* It NEVER carries `message`, `stack`, `cause` or `details`. A name that is not a short identifier (a hostile or free-text `name`) becomes
|
|
21
|
+
* `NonError`; a status outside 100-599 is dropped; a code outside the closed token shape is dropped.
|
|
22
|
+
*/
|
|
23
|
+
export function errForLog(err: unknown): string {
|
|
24
|
+
const e = (err !== null && typeof err === 'object' ? err : {}) as { name?: unknown; status?: unknown; code?: unknown };
|
|
25
|
+
const name = typeof e.name === 'string' && /^[A-Za-z0-9_]{1,48}$/.test(e.name) ? e.name : 'NonError';
|
|
26
|
+
const status = typeof e.status === 'number' && Number.isInteger(e.status) && e.status >= 100 && e.status <= 599 ? ` status=${e.status}` : '';
|
|
27
|
+
const closed = closedCodeForLog(e.code);
|
|
28
|
+
const code = closed ? ` code=${closed}` : '';
|
|
29
|
+
return `error=${name}${status}${code}`;
|
|
30
|
+
}
|
package/src/lib/redis.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// E:\Repos\PayEz-Next-MVP\packages\next-mvp\src\lib\redis.ts
|
|
2
2
|
import Redis, { RedisOptions } from 'ioredis';
|
|
3
|
+
import { errForLog } from './log-safe';
|
|
3
4
|
|
|
4
5
|
let client: Redis | null = null;
|
|
5
6
|
|
|
@@ -76,7 +77,7 @@ function createClient(): Redis {
|
|
|
76
77
|
// unhandled EventEmitter error, which can take the whole PROCESS down -
|
|
77
78
|
// a Redis blip becoming a dead server is worse than the spinner was.
|
|
78
79
|
instance.on('error', (err) => {
|
|
79
|
-
console.error(`[redis] PATH FAILURE ${new Date().toISOString()}: ${
|
|
80
|
+
console.error(`[redis] PATH FAILURE ${new Date().toISOString()}: ${errForLog(err)}`);
|
|
80
81
|
});
|
|
81
82
|
|
|
82
83
|
// 185121 line 3: the 16:05 event left no named trigger because ONLY 'error'
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The IdP fetch timeout and the "refresh in doubt" marker (BAPert 76397 decision 3, corrected by 76403; Core facts DotNetPert 76400).
|
|
3
|
+
*
|
|
4
|
+
* THE PROBLEM. Core consumes a refresh token with one atomic UPDATE and has NO reuse grace: re-presenting a used token revokes every newer
|
|
5
|
+
* refresh token the USER holds, on every device (RevokeDescendantTokens). So when OUR fetch to the IdP times out, we do not know whether Core
|
|
6
|
+
* consumed the token and minted a replacement whose answer we never saw. The store still holds the token we SENT (nothing was persisted).
|
|
7
|
+
* 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.
|
|
8
|
+
*
|
|
9
|
+
* 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
|
|
10
|
+
* 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
|
|
11
|
+
* IdP: terminal REFRESH_OUTCOME_UNKNOWN / REAUTHENTICATE. One login on one session is better than a user-wide revocation. The marker is
|
|
12
|
+
* dropped by the same write that stores a DIFFERENT refresh token (session-store `dropStaleDiscardMarker`); nothing tries to resolve the doubt
|
|
13
|
+
* without a login.
|
|
14
|
+
*
|
|
15
|
+
* Leaf module: no imports from the rest of the package, so session-store can use it without a cycle.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from 'crypto';
|
|
18
|
+
|
|
19
|
+
/** 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. */
|
|
20
|
+
export const REFRESH_FETCH_TIMEOUT_MS = 10_000;
|
|
21
|
+
|
|
22
|
+
/** The typed terminal answer for an attempt that would re-send an in-doubt token. */
|
|
23
|
+
export const REFRESH_OUTCOME_UNKNOWN_CODE = 'REFRESH_OUTCOME_UNKNOWN';
|
|
24
|
+
|
|
25
|
+
/** The typed answer for the timed-out call itself. */
|
|
26
|
+
export const UPSTREAM_TIMEOUT_CODE = 'UPSTREAM_TIMEOUT';
|
|
27
|
+
|
|
28
|
+
/** A one-way identity for a refresh token. Not reversible and not a prefix. */
|
|
29
|
+
export function refreshTokenIdentity(token: string): string {
|
|
30
|
+
return createHash('sha256').update(token, 'utf8').digest('hex');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The row fields recorded at a timeout. */
|
|
34
|
+
export function inDoubtPatch(sentToken: string): { refreshInDoubtTokenHash: string; refreshInDoubtAt: number } {
|
|
35
|
+
return { refreshInDoubtTokenHash: refreshTokenIdentity(sentToken), refreshInDoubtAt: Date.now() };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** True when the token we are about to send is the one a previous attempt left in doubt. */
|
|
39
|
+
export function isRefreshInDoubt(
|
|
40
|
+
session: { refreshInDoubtTokenHash?: string | null } | null | undefined,
|
|
41
|
+
tokenToSend: string | null | undefined,
|
|
42
|
+
): boolean {
|
|
43
|
+
if (!session || typeof session.refreshInDoubtTokenHash !== 'string' || !session.refreshInDoubtTokenHash) return false;
|
|
44
|
+
if (typeof tokenToSend !== 'string' || tokenToSend.length === 0) return false;
|
|
45
|
+
return refreshTokenIdentity(tokenToSend) === session.refreshInDoubtTokenHash;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** True for the error `AbortSignal.timeout` produces (a DOMException named TimeoutError); AbortError is treated the same. */
|
|
49
|
+
export function isTimeoutError(err: unknown): boolean {
|
|
50
|
+
const name = (err as { name?: string } | null | undefined)?.name;
|
|
51
|
+
return name === 'TimeoutError' || name === 'AbortError';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The ONLY transport error codes that PROVE the request never left this process (BAPert 76433 item 4): the connection was refused, or the
|
|
56
|
+
* name did not resolve. Anything else (ECONNRESET, UND_ERR_SOCKET, a timeout, an error with no code) may have happened AFTER the request was
|
|
57
|
+
* 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
|
|
58
|
+
* missed one costs a user-wide revocation. Do NOT add a code here without proof it can only occur before the request is sent.
|
|
59
|
+
*/
|
|
60
|
+
export const REQUEST_NEVER_LEFT_CODES: ReadonlySet<string> = new Set(['ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN']);
|
|
61
|
+
|
|
62
|
+
/** Every `code` on the error, its `cause`, and the members of an AggregateError (undici wraps the socket error in `cause`). */
|
|
63
|
+
function transportCodes(err: unknown, depth = 0, out: string[] = []): string[] {
|
|
64
|
+
if (!err || typeof err !== 'object' || depth > 4) return out;
|
|
65
|
+
const e = err as { code?: unknown; cause?: unknown; errors?: unknown };
|
|
66
|
+
if (typeof e.code === 'string' && e.code) out.push(e.code);
|
|
67
|
+
if (e.cause) transportCodes(e.cause, depth + 1, out);
|
|
68
|
+
if (Array.isArray(e.errors)) for (const inner of e.errors) transportCodes(inner, depth + 1, out);
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 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
|
|
74
|
+
* is NOT proof, so it is in-doubt.
|
|
75
|
+
*/
|
|
76
|
+
export function requestNeverLeft(err: unknown): boolean {
|
|
77
|
+
const codes = transportCodes(err);
|
|
78
|
+
return codes.length > 0 && codes.every((c) => REQUEST_NEVER_LEFT_CODES.has(c));
|
|
79
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jwtDecode } from './jwt-decode';
|
|
2
2
|
import { tokenRefreshLogger } from '../config/logger';
|
|
3
|
+
import { REFRESH_WINDOW_MS } from './refresh-window';
|
|
3
4
|
|
|
4
5
|
interface RefreshTokenPayload { exp: number; iat: number; jti: string; user_id: string; token_type: string; binding: string; }
|
|
5
6
|
|
|
@@ -38,7 +39,7 @@ export function checkRefreshViability(sessionData: any): RefreshViabilityCheck {
|
|
|
38
39
|
if (typeof expiresAtMs === 'string') expiresAtMs = parseInt(expiresAtMs, 10);
|
|
39
40
|
if (expiresAtMs < 1000000000000) expiresAtMs = expiresAtMs * 1000;
|
|
40
41
|
accessTokenTimeRemaining = Math.floor((expiresAtMs - now) / 1000);
|
|
41
|
-
const bufferSec =
|
|
42
|
+
const bufferSec = REFRESH_WINDOW_MS / 1000; // the one refresh window (lib/refresh-window), in seconds
|
|
42
43
|
accessTokenExpired = accessTokenTimeRemaining <= bufferSec;
|
|
43
44
|
tokenRefreshLogger.debug('[REFRESH_VIABILITY] Access token expiration check', { now, expiresAtMs, accessTokenTimeRemaining, bufferSec, accessTokenExpired });
|
|
44
45
|
}
|
|
@@ -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 const REFRESH_WINDOW_MS = 5 * 60 * 1000;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { knownPasswordRules } from './password-rules';
|
|
18
|
+
import { errForLog } from './log-safe';
|
|
18
19
|
|
|
19
20
|
export interface IdpErrorProjection {
|
|
20
21
|
code?: string;
|
|
@@ -25,10 +26,14 @@ export interface IdpErrorProjection {
|
|
|
25
26
|
|
|
26
27
|
const str = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined);
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Log an exception with a fresh correlationId and return the id for the response body.
|
|
31
|
+
* The log line carries the error's NAME and closed code/status only (errForLog), never its message, stack or cause: those can hold an
|
|
32
|
+
* email, a phone number, a token or a quoted request body (PII-in-logs, BAPert 76553 group 1). The correlationId still ties the response to the line.
|
|
33
|
+
*/
|
|
29
34
|
export function logExceptionWithCorrelation(tag: string, err: unknown): string {
|
|
30
35
|
const correlationId = globalThis.crypto.randomUUID();
|
|
31
|
-
console.error(tag, { correlationId, error: err });
|
|
36
|
+
console.error(tag, { correlationId, error: errForLog(err) });
|
|
32
37
|
return correlationId;
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
|
|
10
|
+
/** Longest trace id that is logged. 64 covers a UUID (36) and common trace formats; longer input is cut, not rejected. */
|
|
11
|
+
export const MAX_TRACE_ID_LENGTH = 64;
|
|
12
|
+
|
|
13
|
+
// C0 controls (incl. CR, LF, TAB, ESC), DEL, C1 controls (incl. NEL U+0085), and the Unicode line/paragraph separators U+2028 / U+2029.
|
|
14
|
+
// eslint-disable-next-line no-control-regex
|
|
15
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g;
|
|
16
|
+
|
|
17
|
+
export function sanitizeTraceId(value: unknown): string | undefined {
|
|
18
|
+
if (typeof value !== 'string') return undefined;
|
|
19
|
+
const cleaned = value.replace(CONTROL_CHARS, '').slice(0, MAX_TRACE_ID_LENGTH);
|
|
20
|
+
return cleaned.length > 0 ? cleaned : undefined;
|
|
21
|
+
}
|
package/src/lib/session-store.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { SessionData } from '../models/SessionModel';
|
|
|
14
14
|
import { getSessionPrefix, getRefreshLockPrefix, getAppSlug } from './app-slug';
|
|
15
15
|
import { extractKidFromToken, decodeIdpAccessToken } from '../auth/utils/token-utils';
|
|
16
16
|
import { tokenLogId } from './token-log-id';
|
|
17
|
+
import { refreshTokenIdentity } from './refresh-in-doubt';
|
|
18
|
+
import { errForLog } from './log-safe';
|
|
17
19
|
|
|
18
20
|
// Re-export SessionData for consumers
|
|
19
21
|
export type { SessionData } from '../models/SessionModel';
|
|
@@ -64,7 +66,7 @@ export async function createSession(data: SessionData): Promise<string> {
|
|
|
64
66
|
.setex(versionKey, SESSION_TTL, '1')
|
|
65
67
|
.exec();
|
|
66
68
|
} catch (error) {
|
|
67
|
-
console.error('[SESSION-STORE] Failed to create session:', error);
|
|
69
|
+
console.error('[SESSION-STORE] Failed to create session:', errForLog(error));
|
|
68
70
|
throw error;
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -293,12 +295,23 @@ export async function deleteSessionEverywhere(sessionToken: string, appSlug?: st
|
|
|
293
295
|
* today; this is the defence BAPert ruled to keep anyway.
|
|
294
296
|
*/
|
|
295
297
|
function dropStaleDiscardMarker<T extends object>(session: T): T {
|
|
296
|
-
|
|
298
|
+
let s = session as any;
|
|
297
299
|
if (typeof s.idpRefreshToken === 'string' && s.idpRefreshToken.length > 0 && (s.refreshTokenClearedReason !== undefined || s.refreshTokenClearedAt !== undefined)) {
|
|
298
300
|
const { refreshTokenClearedReason: _reason, refreshTokenClearedAt: _at, ...rest } = s;
|
|
299
|
-
|
|
301
|
+
s = rest;
|
|
300
302
|
}
|
|
301
|
-
|
|
303
|
+
// 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
|
|
304
|
+
// whose identity differs from the one left in doubt. A write that stores the SAME token (any caller spreading the whole row back) does
|
|
305
|
+
// NOT clear it: that token is still the in-doubt one.
|
|
306
|
+
if (
|
|
307
|
+
typeof s.refreshInDoubtTokenHash === 'string' &&
|
|
308
|
+
typeof s.idpRefreshToken === 'string' && s.idpRefreshToken.length > 0 &&
|
|
309
|
+
refreshTokenIdentity(s.idpRefreshToken) !== s.refreshInDoubtTokenHash
|
|
310
|
+
) {
|
|
311
|
+
const { refreshInDoubtTokenHash: _hash, refreshInDoubtAt: _doubtAt, ...rest } = s;
|
|
312
|
+
s = rest;
|
|
313
|
+
}
|
|
314
|
+
return s as T;
|
|
302
315
|
}
|
|
303
316
|
|
|
304
317
|
export async function setSession(sessionToken: string, data: SessionData): Promise<void> {
|
package/src/lib/site-logger.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { getRedis } from './redis';
|
|
14
|
+
import { closedCodeForLog, errForLog } from './log-safe';
|
|
14
15
|
|
|
15
16
|
// Redis key for site logs (separate from data_logs)
|
|
16
17
|
const REDIS_SITE_LOG_KEY = 'vibe:site-logs:pending';
|
|
@@ -74,11 +75,11 @@ export async function logSiteEvent(entry: SiteLogEntry): Promise<void> {
|
|
|
74
75
|
console.log('[site-logger] Redis push success, queue length:', result);
|
|
75
76
|
redis.expire(REDIS_SITE_LOG_KEY, REDIS_LOG_TTL).catch(() => {});
|
|
76
77
|
}).catch(err => {
|
|
77
|
-
console.error('[site-logger] Redis push failed:', err
|
|
78
|
+
console.error('[site-logger] Redis push failed:', errForLog(err));
|
|
78
79
|
});
|
|
79
80
|
} catch (error) {
|
|
80
81
|
// Fail silently - logging should never break the app
|
|
81
|
-
console.error('[site-logger] Failed to log event:', error);
|
|
82
|
+
console.error('[site-logger] Failed to log event:', errForLog(error));
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -120,12 +121,14 @@ export const siteEvents = {
|
|
|
120
121
|
});
|
|
121
122
|
},
|
|
122
123
|
|
|
123
|
-
|
|
124
|
+
// BREAKING (PII-in-logs group 3b, BAPert 76553 Q2): no `email` parameter. A failed login is told apart by a CLOSED reason code (UPPER_SNAKE),
|
|
125
|
+
// never by the address that was tried; anything that is not a closed code is stored as UNSPECIFIED. A caller that still passes `email` fails to compile.
|
|
126
|
+
loginFailed: (opts: { reason: string; url?: string; user_agent?: string; ip_address?: string }) => {
|
|
124
127
|
logSiteEvent({
|
|
125
128
|
level: 'warn',
|
|
126
129
|
category: 'auth',
|
|
127
130
|
message: 'Login failed',
|
|
128
|
-
context: { reason: opts.reason
|
|
131
|
+
context: { reason: closedCodeForLog(opts.reason) ?? 'UNSPECIFIED' },
|
|
129
132
|
url: opts.url || '/auth/login',
|
|
130
133
|
user_agent: opts.user_agent,
|
|
131
134
|
ip_address: opts.ip_address,
|
|
@@ -214,12 +217,15 @@ export const siteEvents = {
|
|
|
214
217
|
});
|
|
215
218
|
},
|
|
216
219
|
|
|
217
|
-
|
|
220
|
+
// BREAKING (PII-in-logs group 3b, BAPert 76553 Q2): takes a CLOSED code, not caller text, an error string or a stack. The stored message is the fixed
|
|
221
|
+
// sentence 'Application error'; the code (UPPER_SNAKE, else UNSPECIFIED) goes in context. A caller that still passes `message`, `error` or `stack`
|
|
222
|
+
// fails to compile.
|
|
223
|
+
error: (opts: { code: string; user_id?: string | number; url?: string; user_agent?: string; ip_address?: string }) => {
|
|
218
224
|
logSiteEvent({
|
|
219
225
|
level: 'error',
|
|
220
226
|
category: 'error',
|
|
221
|
-
message:
|
|
222
|
-
context: {
|
|
227
|
+
message: 'Application error',
|
|
228
|
+
context: { code: closedCodeForLog(opts.code) ?? 'UNSPECIFIED' },
|
|
223
229
|
user_id: opts.user_id,
|
|
224
230
|
url: opts.url,
|
|
225
231
|
user_agent: opts.user_agent,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// ========================================================================================
|
|
10
10
|
|
|
11
11
|
import { authClient } from '../client/better-auth-client';
|
|
12
|
+
import { errForLog } from './log-safe';
|
|
12
13
|
|
|
13
14
|
/** Unwrap Better Auth getSession() to a flat session-like object for backward compat */
|
|
14
15
|
async function getSession(): Promise<any> {
|
|
@@ -147,7 +148,7 @@ function scheduleLoginRedirect(isImmediate = false) {
|
|
|
147
148
|
console.warn(`🔄 Redirecting to login with return URL: ${returnUrl}`);
|
|
148
149
|
window.location.href = `/account-auth/login?returnUrl=${returnUrl}`;
|
|
149
150
|
} catch (error) {
|
|
150
|
-
console.error('❌ Error during login redirect:', error);
|
|
151
|
+
console.error('❌ Error during login redirect:', errForLog(error));
|
|
151
152
|
// Final fallback
|
|
152
153
|
try {
|
|
153
154
|
window.location.href = '/account-auth/login';
|
|
@@ -721,7 +722,7 @@ class StandardizedClientApiService {
|
|
|
721
722
|
}
|
|
722
723
|
|
|
723
724
|
// Wrap unknown errors as network errors
|
|
724
|
-
console.error('❌ API request failed:', error);
|
|
725
|
+
console.error('❌ API request failed:', errForLog(error));
|
|
725
726
|
throw new ApiNetworkError(
|
|
726
727
|
error instanceof Error ? error.message : 'Network error',
|
|
727
728
|
0,
|
|
@@ -26,9 +26,11 @@ import { getSession as getBetterAuthSession } from '../server/auth';
|
|
|
26
26
|
import { getRedis } from './redis';
|
|
27
27
|
import { getAppSlug } from './app-slug';
|
|
28
28
|
import { tokenLogId } from './token-log-id';
|
|
29
|
+
import { REFRESH_WINDOW_MS } from './refresh-window';
|
|
30
|
+
import { errForLog } from './log-safe';
|
|
29
31
|
|
|
30
|
-
//
|
|
31
|
-
const REFRESH_THRESHOLD_MS =
|
|
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_MS;
|
|
32
34
|
|
|
33
35
|
// Concurrent refresh handling configuration
|
|
34
36
|
const CONCURRENT_REFRESH_POLL_INTERVAL_MS = 200; // How often to poll session during concurrent refresh
|
|
@@ -433,7 +435,7 @@ export async function ensureFreshToken(
|
|
|
433
435
|
}
|
|
434
436
|
} catch (e) {
|
|
435
437
|
// If we can't decode, proceed with normal logic
|
|
436
|
-
console.warn('[TOKEN_LIFECYCLE] Could not validate JWT exp claim:', e);
|
|
438
|
+
console.warn('[TOKEN_LIFECYCLE] Could not validate JWT exp claim:', errForLog(e));
|
|
437
439
|
}
|
|
438
440
|
}
|
|
439
441
|
|
|
@@ -511,7 +513,7 @@ export async function ensureFreshToken(
|
|
|
511
513
|
sessionData,
|
|
512
514
|
};
|
|
513
515
|
} catch (error) {
|
|
514
|
-
console.error('[TOKEN_LIFECYCLE] Error:', error);
|
|
516
|
+
console.error('[TOKEN_LIFECYCLE] Error:', errForLog(error));
|
|
515
517
|
return {
|
|
516
518
|
success: false,
|
|
517
519
|
error: 'NO_SESSION',
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
resetSecret,
|
|
33
33
|
signedResetHref,
|
|
34
34
|
} from '../auth/session-reset-link';
|
|
35
|
+
import { errForLog } from '../lib/log-safe';
|
|
35
36
|
|
|
36
37
|
// =============================================================================
|
|
37
38
|
// TYPES
|
|
@@ -1037,7 +1038,7 @@ async function handleAllow(
|
|
|
1037
1038
|
);
|
|
1038
1039
|
}
|
|
1039
1040
|
} catch (error) {
|
|
1040
|
-
console.error('[MIDDLEWARE] RBAC error:', error);
|
|
1041
|
+
console.error('[MIDDLEWARE] RBAC error:', errForLog(error));
|
|
1041
1042
|
|
|
1042
1043
|
// In development, fail open
|
|
1043
1044
|
if (process.env.NODE_ENV !== 'production') {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errForLog } from '../lib/log-safe';
|
|
1
2
|
/**
|
|
2
3
|
* Page RBAC Check Module
|
|
3
4
|
*
|
|
@@ -262,7 +263,7 @@ export async function checkPagePermission(
|
|
|
262
263
|
};
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
console.error('[RBAC] Proxy error:', error);
|
|
266
|
+
console.error('[RBAC] Proxy error:', errForLog(error));
|
|
266
267
|
return {
|
|
267
268
|
allowed: false,
|
|
268
269
|
reason: 'rbac_service_unavailable',
|
|
@@ -59,6 +59,13 @@ export interface SessionData {
|
|
|
59
59
|
|
|
60
60
|
/** When the IDP refresh token expires (Unix timestamp ms) */
|
|
61
61
|
idpRefreshTokenExpires?: number;
|
|
62
|
+
/**
|
|
63
|
+
* 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
|
|
64
|
+
* token), and when. A later attempt whose token-to-send matches refuses without calling the IdP. Cleared by the write that stores a different
|
|
65
|
+
* refresh token.
|
|
66
|
+
*/
|
|
67
|
+
refreshInDoubtTokenHash?: string;
|
|
68
|
+
refreshInDoubtAt?: number;
|
|
62
69
|
|
|
63
70
|
/** Decoded IDP access token claims (for quick access without re-decoding) */
|
|
64
71
|
decodedAccessToken?: any;
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from '../../lib/app-slug';
|
|
28
28
|
import { siteEvents, getClientIp } from '../../lib/site-logger';
|
|
29
29
|
import { logExceptionWithCorrelation } from '../../lib/safe-error-response';
|
|
30
|
+
import { errForLog } from '../../lib/log-safe';
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* 4.8.9: answered (status 500) when a sign-out did not finish. 4.8.10 (BAPert 76297): TWO sentences, one per cause, each true for its
|
|
@@ -79,7 +80,13 @@ export async function POST(req: NextRequest) {
|
|
|
79
80
|
await revokeSessionAtIdp(req);
|
|
80
81
|
} catch (error) {
|
|
81
82
|
idpRevokeFailed = true;
|
|
82
|
-
|
|
83
|
+
// "STILL LIVE" only when the IdP showed it (a forced refresh succeeded); otherwise say what is known: it was not confirmed.
|
|
84
|
+
console.error(
|
|
85
|
+
(error as { grantLive?: boolean } | null)?.grantLive === true
|
|
86
|
+
? '[LOGOUT_ROUTE] IdP revoke failed - the IdP refresh token is STILL LIVE:'
|
|
87
|
+
: '[LOGOUT_ROUTE] IdP revoke could not be confirmed (the grant may already be dead):',
|
|
88
|
+
errForLog(error),
|
|
89
|
+
);
|
|
83
90
|
}
|
|
84
91
|
}
|
|
85
92
|
|
|
@@ -91,7 +98,7 @@ export async function POST(req: NextRequest) {
|
|
|
91
98
|
console.info('[LOGOUT_ROUTE] Session deleted from store');
|
|
92
99
|
} catch (error) {
|
|
93
100
|
localDeleteFailed = true;
|
|
94
|
-
console.error('[LOGOUT_ROUTE] Failed to delete session - the server session is STILL LIVE:', error);
|
|
101
|
+
console.error('[LOGOUT_ROUTE] Failed to delete session - the server session is STILL LIVE:', errForLog(error));
|
|
95
102
|
}
|
|
96
103
|
}
|
|
97
104
|
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { NextResponse } from 'next/server';
|
|
31
31
|
import { getIDPClientConfig } from '../../lib/idp-client-config';
|
|
32
32
|
import { secretSafeJson } from '../../lib/never-in-response';
|
|
33
|
+
import { errForLog } from '../../lib/log-safe';
|
|
33
34
|
|
|
34
35
|
export interface PublicAuthSettings {
|
|
35
36
|
// OAuth providers enabled for this client
|
|
@@ -95,7 +96,7 @@ export async function GET() {
|
|
|
95
96
|
config = await getIDPClientConfig();
|
|
96
97
|
} catch (error) {
|
|
97
98
|
// Keep the loud line - it is what finally unmasked this in production.
|
|
98
|
-
console.error('[AUTH_SETTINGS] Failed to get settings:', error);
|
|
99
|
+
console.error('[AUTH_SETTINGS] Failed to get settings:', errForLog(error));
|
|
99
100
|
// The `degraded: true` shape that used to live here (240447) is gone
|
|
100
101
|
// deliberately. It still answered `success: true` with a full invented
|
|
101
102
|
// policy and merely LABELLED the guess - which BAPert v32 rules out:
|
package/src/server/auth-guard.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { decodeSession } from './decode-session';
|
|
|
14
14
|
import { getIDPClientConfig } from '../lib/idp-client-config';
|
|
15
15
|
import { getSessionCookieName, getSecureSessionCookieName } from '../lib/app-slug';
|
|
16
16
|
import type { SessionData } from '../lib/session-store';
|
|
17
|
+
import { errForLog } from '../lib/log-safe';
|
|
17
18
|
|
|
18
19
|
// =============================================================================
|
|
19
20
|
// TYPES
|
|
@@ -89,7 +90,7 @@ export async function authGuard(options?: AuthGuardOptions): Promise<AuthGuardRe
|
|
|
89
90
|
decoded = await decodeSession();
|
|
90
91
|
} catch (error) {
|
|
91
92
|
// Redis unreachable or startup failure → fail closed
|
|
92
|
-
console.error('[AUTH-GUARD] Session decode failed (service error):',
|
|
93
|
+
console.error('[AUTH-GUARD] Session decode failed (service error):', errForLog(error));
|
|
93
94
|
redirect(serviceUnavailableUrl);
|
|
94
95
|
}
|
|
95
96
|
|
|
@@ -122,7 +123,7 @@ export async function authGuard(options?: AuthGuardOptions): Promise<AuthGuardRe
|
|
|
122
123
|
requires2FA = config.authSettings?.require2FA ?? true;
|
|
123
124
|
} catch (error) {
|
|
124
125
|
// If we can't check 2FA config, fail closed
|
|
125
|
-
console.error('[AUTH-GUARD] 2FA config check failed:',
|
|
126
|
+
console.error('[AUTH-GUARD] 2FA config check failed:', errForLog(error));
|
|
126
127
|
redirect(serviceUnavailableUrl);
|
|
127
128
|
}
|
|
128
129
|
|
|
@@ -186,7 +187,7 @@ export async function authGuard(options?: AuthGuardOptions): Promise<AuthGuardRe
|
|
|
186
187
|
if (error && typeof error === 'object' && 'digest' in error) {
|
|
187
188
|
throw error;
|
|
188
189
|
}
|
|
189
|
-
console.error(`[AUTH-GUARD] Custom check "${check.name}" error:`,
|
|
190
|
+
console.error(`[AUTH-GUARD] Custom check "${check.name}" error:`, errForLog(error));
|
|
190
191
|
redirect(serviceUnavailableUrl);
|
|
191
192
|
}
|
|
192
193
|
}
|
package/src/server/auth.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type SessionData,
|
|
15
15
|
} from '../lib/session-store';
|
|
16
16
|
import { tokenLogId } from '../lib/token-log-id';
|
|
17
|
+
import { errForLog } from '../lib/log-safe';
|
|
17
18
|
import { resolveVerifiedSessionTokenFromRequest } from './verified-session-token';
|
|
18
19
|
|
|
19
20
|
export type IdpTokenResult =
|
|
@@ -134,20 +135,14 @@ export async function getAuthInstance() {
|
|
|
134
135
|
* `{error:'NO_SESSION'}`, and getSession() swallowed every Redis error into a bare `catch {}`. These helpers name
|
|
135
136
|
* the origin and make a swallowed read audible. Hard rules: every helper is wrapped so it can NEVER throw or change
|
|
136
137
|
* a return value; nothing here ever logs a token, a cookie VALUE or a Redis value - only a tokenLogId() fingerprint,
|
|
137
|
-
* cookie NAMES, booleans, and an error
|
|
138
|
+
* cookie NAMES, booleans, and an error NAME with its closed code/status (errForLog; never the message).
|
|
138
139
|
* -------------------------------------------------------------------------------------------------------------- */
|
|
139
140
|
|
|
140
|
-
/**
|
|
141
|
-
|
|
142
|
-
const raw = v instanceof Error ? v.message : String(v ?? '');
|
|
143
|
-
return raw.replace(/[A-Za-z0-9_\-.+/=]{24,}/g, '[redacted]').slice(0, 80);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** `catch {}` sites on the session read path: still swallowed, now audible. */
|
|
141
|
+
/** `catch {}` sites on the session read path: still swallowed, now audible. The line names the error and its closed code/status only: the
|
|
142
|
+
* message is dropped (BAPert 76553: a redacted-and-truncated message still leaked host names, ports and quoted input). */
|
|
147
143
|
function logSessionReadSwallowed(path: 'primary' | 'A' | 'B', err: unknown): void {
|
|
148
144
|
try {
|
|
149
|
-
|
|
150
|
-
console.warn(`[server-auth] SESSION_READ_SWALLOWED path=${path} err=${name} msg=${scrubForLog(err)}`);
|
|
145
|
+
console.warn(`[server-auth] SESSION_READ_SWALLOWED path=${path} ${errForLog(err)}`);
|
|
151
146
|
} catch {
|
|
152
147
|
/* a diagnostic must never change the outcome */
|
|
153
148
|
}
|
|
@@ -407,7 +402,7 @@ export interface FreshIdpTokenConfig {
|
|
|
407
402
|
idpBaseUrl: string;
|
|
408
403
|
clientId: string;
|
|
409
404
|
refreshEndpoint?: string;
|
|
410
|
-
/** Refresh if the access token is within this many ms of expiry. Default
|
|
405
|
+
/** Refresh if the access token is within this many ms of expiry. Default REFRESH_WINDOW_MS (5 min, lib/refresh-window). */
|
|
411
406
|
safetyWindowMs?: number;
|
|
412
407
|
}
|
|
413
408
|
|
|
@@ -15,6 +15,7 @@ import { ensureInitialized } from '../lib/startup-init';
|
|
|
15
15
|
import { extractSessionTokenFromCookieValue } from '../lib/session-cookie-shape';
|
|
16
16
|
import { resolveVerifiedSessionCookie } from './verified-session-token';
|
|
17
17
|
import { tokenLogId } from '../lib/token-log-id';
|
|
18
|
+
import { errForLog } from '../lib/log-safe';
|
|
18
19
|
|
|
19
20
|
export interface DecodedSession {
|
|
20
21
|
sessionData: SessionData;
|
|
@@ -197,7 +198,7 @@ async function tryBetterAuthSession(
|
|
|
197
198
|
|
|
198
199
|
return { sessionData, jwtPayload };
|
|
199
200
|
} catch (error) {
|
|
200
|
-
console.warn('[DECODE-SESSION] Better Auth session check failed:',
|
|
201
|
+
console.warn('[DECODE-SESSION] Better Auth session check failed:', errForLog(error));
|
|
201
202
|
return null;
|
|
202
203
|
}
|
|
203
204
|
}
|
|
@@ -303,7 +304,7 @@ export async function decodeSession(
|
|
|
303
304
|
jwtPayload: (payload ?? { sessionToken }) as DecodedSession['jwtPayload'],
|
|
304
305
|
};
|
|
305
306
|
} catch (error) {
|
|
306
|
-
console.error('[DECODE-SESSION] Error:',
|
|
307
|
+
console.error('[DECODE-SESSION] Error:', errForLog(error));
|
|
307
308
|
return null;
|
|
308
309
|
}
|
|
309
310
|
}
|
package/src/server/with-auth.ts
CHANGED
|
@@ -17,6 +17,7 @@ import 'server-only';
|
|
|
17
17
|
import { NextRequest, NextResponse } from 'next/server';
|
|
18
18
|
import { decodeSession } from './decode-session';
|
|
19
19
|
import type { SessionData } from '../lib/session-store';
|
|
20
|
+
import { errForLog } from '../lib/log-safe';
|
|
20
21
|
|
|
21
22
|
// =============================================================================
|
|
22
23
|
// TYPES
|
|
@@ -83,7 +84,7 @@ export function withAuth(
|
|
|
83
84
|
|
|
84
85
|
return handler(req, auth);
|
|
85
86
|
} catch (error) {
|
|
86
|
-
console.error('[WITH-AUTH] Error:',
|
|
87
|
+
console.error('[WITH-AUTH] Error:', errForLog(error));
|
|
87
88
|
return NextResponse.json(
|
|
88
89
|
{ error: 'Internal Server Error', message: 'Auth check failed' },
|
|
89
90
|
{ status: 500 }
|
package/src/stores/authStore.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
getSecureCsrfCookieName
|
|
28
28
|
} from '../lib/app-slug';
|
|
29
29
|
import { tokenLogId } from '../lib/token-log-id';
|
|
30
|
+
import { REFRESH_WINDOW_MS } from '../lib/refresh-window';
|
|
30
31
|
|
|
31
32
|
// ===============================
|
|
32
33
|
// INTERFACES & TYPES
|
|
@@ -752,8 +753,8 @@ export const useAuthStore = create<AuthStore>()(
|
|
|
752
753
|
const now = Date.now();
|
|
753
754
|
const timeUntilExpiry = tokenExpiry - now;
|
|
754
755
|
|
|
755
|
-
// Token should be fresh (not expiring
|
|
756
|
-
if (timeUntilExpiry <
|
|
756
|
+
// Token should be fresh (not expiring within the one refresh window)
|
|
757
|
+
if (timeUntilExpiry < REFRESH_WINDOW_MS) {
|
|
757
758
|
throw new Error(`Fresh token still expires soon: ${timeUntilExpiry}ms`);
|
|
758
759
|
}
|
|
759
760
|
|