@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
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.jwtDecode = jwtDecode;
|
|
8
|
+
const log_safe_1 = require("./log-safe");
|
|
8
9
|
// Decode base64url
|
|
9
10
|
function base64urlDecode(base64url) {
|
|
10
11
|
try {
|
|
@@ -40,7 +41,7 @@ function jwtDecode(token) {
|
|
|
40
41
|
return parsedPayload;
|
|
41
42
|
}
|
|
42
43
|
catch (e) {
|
|
43
|
-
console.error('[JWT] Decode failed:',
|
|
44
|
+
console.error('[JWT] Decode failed:', (0, log_safe_1.errForLog)(e));
|
|
44
45
|
return null;
|
|
45
46
|
}
|
|
46
47
|
}
|
package/dist/lib/jwt-decode.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.jwtDecode = jwtDecode;
|
|
|
4
4
|
exports.decodeJwtHeader = decodeJwtHeader;
|
|
5
5
|
exports.extractKidFromToken = extractKidFromToken;
|
|
6
6
|
const jwt_decode_1 = require("jwt-decode");
|
|
7
|
+
const log_safe_1 = require("./log-safe");
|
|
7
8
|
/**
|
|
8
9
|
* Decode JWT payload (standard claims).
|
|
9
10
|
* This is a thin wrapper around jwt-decode library.
|
|
@@ -40,7 +41,7 @@ function decodeJwtHeader(token) {
|
|
|
40
41
|
return JSON.parse(headerJson);
|
|
41
42
|
}
|
|
42
43
|
catch (error) {
|
|
43
|
-
console.error('[JWT_DECODE] Failed to decode JWT header:', error);
|
|
44
|
+
console.error('[JWT_DECODE] Failed to decode JWT header:', (0, log_safe_1.errForLog)(error));
|
|
44
45
|
return null;
|
|
45
46
|
}
|
|
46
47
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
/** An upstream `code` is loggable only if it is a closed token (UPPER_SNAKE). Anything else, including free text, becomes null. */
|
|
13
|
+
export declare function closedCodeForLog(code: unknown): string | null;
|
|
14
|
+
/**
|
|
15
|
+
* Describe a caught error for a log line: `error=<Name> [status=<n>] [code=<CLOSED_CODE>]`.
|
|
16
|
+
* It NEVER carries `message`, `stack`, `cause` or `details`. A name that is not a short identifier (a hostile or free-text `name`) becomes
|
|
17
|
+
* `NonError`; a status outside 100-599 is dropped; a code outside the closed token shape is dropped.
|
|
18
|
+
*/
|
|
19
|
+
export declare function errForLog(err: unknown): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 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
|
|
4
|
+
* portal's api-error-response.ts, BAPert 76470).
|
|
5
|
+
*
|
|
6
|
+
* Server logs persist and are read beyond the person who made the request. An error's `message`, `stack`, `cause` and `details` can carry an
|
|
7
|
+
* 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
|
|
8
|
+
* 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
|
|
9
|
+
* shape; anything that does not match is dropped, not truncated.
|
|
10
|
+
*
|
|
11
|
+
* No imports on purpose: this is a leaf, loaded by modules that must not pull in anything else.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.closedCodeForLog = closedCodeForLog;
|
|
15
|
+
exports.errForLog = errForLog;
|
|
16
|
+
/** An upstream `code` is loggable only if it is a closed token (UPPER_SNAKE). Anything else, including free text, becomes null. */
|
|
17
|
+
function closedCodeForLog(code) {
|
|
18
|
+
return typeof code === 'string' && /^[A-Z0-9_]{1,64}$/.test(code) ? code : null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Describe a caught error for a log line: `error=<Name> [status=<n>] [code=<CLOSED_CODE>]`.
|
|
22
|
+
* It NEVER carries `message`, `stack`, `cause` or `details`. A name that is not a short identifier (a hostile or free-text `name`) becomes
|
|
23
|
+
* `NonError`; a status outside 100-599 is dropped; a code outside the closed token shape is dropped.
|
|
24
|
+
*/
|
|
25
|
+
function errForLog(err) {
|
|
26
|
+
const e = (err !== null && typeof err === 'object' ? err : {});
|
|
27
|
+
const name = typeof e.name === 'string' && /^[A-Za-z0-9_]{1,48}$/.test(e.name) ? e.name : 'NonError';
|
|
28
|
+
const status = typeof e.status === 'number' && Number.isInteger(e.status) && e.status >= 100 && e.status <= 599 ? ` status=${e.status}` : '';
|
|
29
|
+
const closed = closedCodeForLog(e.code);
|
|
30
|
+
const code = closed ? ` code=${closed}` : '';
|
|
31
|
+
return `error=${name}${status}${code}`;
|
|
32
|
+
}
|
package/dist/lib/redis.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.redis = void 0;
|
|
|
7
7
|
exports.getRedis = getRedis;
|
|
8
8
|
// E:\Repos\PayEz-Next-MVP\packages\next-mvp\src\lib\redis.ts
|
|
9
9
|
const ioredis_1 = __importDefault(require("ioredis"));
|
|
10
|
+
const log_safe_1 = require("./log-safe");
|
|
10
11
|
let client = null;
|
|
11
12
|
// ── 183496: fail FAST and NAMED, never a 20s spinner ───────────────────────
|
|
12
13
|
// The DEFAULTS were the defect (Nextpert-Scout's killable-proxy measurement,
|
|
@@ -75,7 +76,7 @@ function createClient() {
|
|
|
75
76
|
// unhandled EventEmitter error, which can take the whole PROCESS down -
|
|
76
77
|
// a Redis blip becoming a dead server is worse than the spinner was.
|
|
77
78
|
instance.on('error', (err) => {
|
|
78
|
-
console.error(`[redis] PATH FAILURE ${new Date().toISOString()}: ${
|
|
79
|
+
console.error(`[redis] PATH FAILURE ${new Date().toISOString()}: ${(0, log_safe_1.errForLog)(err)}`);
|
|
79
80
|
});
|
|
80
81
|
// 185121 line 3: the 16:05 event left no named trigger because ONLY 'error'
|
|
81
82
|
// was listened for. A reconnect or a clean end is silent in the artifact,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** 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. */
|
|
2
|
+
export declare const REFRESH_FETCH_TIMEOUT_MS = 10000;
|
|
3
|
+
/** The typed terminal answer for an attempt that would re-send an in-doubt token. */
|
|
4
|
+
export declare const REFRESH_OUTCOME_UNKNOWN_CODE = "REFRESH_OUTCOME_UNKNOWN";
|
|
5
|
+
/** The typed answer for the timed-out call itself. */
|
|
6
|
+
export declare const UPSTREAM_TIMEOUT_CODE = "UPSTREAM_TIMEOUT";
|
|
7
|
+
/** A one-way identity for a refresh token. Not reversible and not a prefix. */
|
|
8
|
+
export declare function refreshTokenIdentity(token: string): string;
|
|
9
|
+
/** The row fields recorded at a timeout. */
|
|
10
|
+
export declare function inDoubtPatch(sentToken: string): {
|
|
11
|
+
refreshInDoubtTokenHash: string;
|
|
12
|
+
refreshInDoubtAt: number;
|
|
13
|
+
};
|
|
14
|
+
/** True when the token we are about to send is the one a previous attempt left in doubt. */
|
|
15
|
+
export declare function isRefreshInDoubt(session: {
|
|
16
|
+
refreshInDoubtTokenHash?: string | null;
|
|
17
|
+
} | null | undefined, tokenToSend: string | null | undefined): boolean;
|
|
18
|
+
/** True for the error `AbortSignal.timeout` produces (a DOMException named TimeoutError); AbortError is treated the same. */
|
|
19
|
+
export declare function isTimeoutError(err: unknown): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The ONLY transport error codes that PROVE the request never left this process (BAPert 76433 item 4): the connection was refused, or the
|
|
22
|
+
* name did not resolve. Anything else (ECONNRESET, UND_ERR_SOCKET, a timeout, an error with no code) may have happened AFTER the request was
|
|
23
|
+
* 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
|
|
24
|
+
* missed one costs a user-wide revocation. Do NOT add a code here without proof it can only occur before the request is sent.
|
|
25
|
+
*/
|
|
26
|
+
export declare const REQUEST_NEVER_LEFT_CODES: ReadonlySet<string>;
|
|
27
|
+
/**
|
|
28
|
+
* 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
|
|
29
|
+
* is NOT proof, so it is in-doubt.
|
|
30
|
+
*/
|
|
31
|
+
export declare function requestNeverLeft(err: unknown): boolean;
|
|
@@ -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
|
+
}
|
|
@@ -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 =
|
|
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;
|
|
@@ -19,7 +19,11 @@ export interface IdpErrorProjection {
|
|
|
19
19
|
resolution?: string;
|
|
20
20
|
request_id?: string;
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Log an exception with a fresh correlationId and return the id for the response body.
|
|
24
|
+
* The log line carries the error's NAME and closed code/status only (errForLog), never its message, stack or cause: those can hold an
|
|
25
|
+
* 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.
|
|
26
|
+
*/
|
|
23
27
|
export declare function logExceptionWithCorrelation(tag: string, err: unknown): string;
|
|
24
28
|
/** Whitelisted projection of an IDP error envelope: code / message / resolution / request_id only. */
|
|
25
29
|
export declare function projectIdpError(json: unknown): IdpErrorProjection;
|
|
@@ -23,11 +23,16 @@ exports.projectResetTokenInvalid = projectResetTokenInvalid;
|
|
|
23
23
|
exports.projectPasswordPolicyRules = projectPasswordPolicyRules;
|
|
24
24
|
exports.projectPasswordBreached = projectPasswordBreached;
|
|
25
25
|
const password_rules_1 = require("./password-rules");
|
|
26
|
+
const log_safe_1 = require("./log-safe");
|
|
26
27
|
const str = (v) => (typeof v === 'string' ? v : undefined);
|
|
27
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Log an exception with a fresh correlationId and return the id for the response body.
|
|
30
|
+
* The log line carries the error's NAME and closed code/status only (errForLog), never its message, stack or cause: those can hold an
|
|
31
|
+
* 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.
|
|
32
|
+
*/
|
|
28
33
|
function logExceptionWithCorrelation(tag, err) {
|
|
29
34
|
const correlationId = globalThis.crypto.randomUUID();
|
|
30
|
-
console.error(tag, { correlationId, error: err });
|
|
35
|
+
console.error(tag, { correlationId, error: (0, log_safe_1.errForLog)(err) });
|
|
31
36
|
return correlationId;
|
|
32
37
|
}
|
|
33
38
|
/** Whitelisted projection of an IDP error envelope: code / message / resolution / request_id only. */
|
|
@@ -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
|
+
}
|
|
@@ -43,6 +43,8 @@ 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");
|
|
47
|
+
const log_safe_1 = require("./log-safe");
|
|
46
48
|
// Use app-slug prefixes for multi-app isolation
|
|
47
49
|
const getSessionKey = (token) => `${(0, app_slug_1.getSessionPrefix)()}${token}`;
|
|
48
50
|
const getRefreshLockKey = (token) => `${(0, app_slug_1.getRefreshLockPrefix)()}${token}`;
|
|
@@ -78,7 +80,7 @@ async function createSession(data) {
|
|
|
78
80
|
.exec();
|
|
79
81
|
}
|
|
80
82
|
catch (error) {
|
|
81
|
-
console.error('[SESSION-STORE] Failed to create session:', error);
|
|
83
|
+
console.error('[SESSION-STORE] Failed to create session:', (0, log_safe_1.errForLog)(error));
|
|
82
84
|
throw error;
|
|
83
85
|
}
|
|
84
86
|
return sessionToken;
|
|
@@ -280,12 +282,21 @@ async function deleteSessionEverywhere(sessionToken, appSlug) {
|
|
|
280
282
|
* today; this is the defence BAPert ruled to keep anyway.
|
|
281
283
|
*/
|
|
282
284
|
function dropStaleDiscardMarker(session) {
|
|
283
|
-
|
|
285
|
+
let s = session;
|
|
284
286
|
if (typeof s.idpRefreshToken === 'string' && s.idpRefreshToken.length > 0 && (s.refreshTokenClearedReason !== undefined || s.refreshTokenClearedAt !== undefined)) {
|
|
285
287
|
const { refreshTokenClearedReason: _reason, refreshTokenClearedAt: _at, ...rest } = s;
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
288
|
+
s = rest;
|
|
289
|
+
}
|
|
290
|
+
// 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
|
|
291
|
+
// whose identity differs from the one left in doubt. A write that stores the SAME token (any caller spreading the whole row back) does
|
|
292
|
+
// NOT clear it: that token is still the in-doubt one.
|
|
293
|
+
if (typeof s.refreshInDoubtTokenHash === 'string' &&
|
|
294
|
+
typeof s.idpRefreshToken === 'string' && s.idpRefreshToken.length > 0 &&
|
|
295
|
+
(0, refresh_in_doubt_1.refreshTokenIdentity)(s.idpRefreshToken) !== s.refreshInDoubtTokenHash) {
|
|
296
|
+
const { refreshInDoubtTokenHash: _hash, refreshInDoubtAt: _doubtAt, ...rest } = s;
|
|
297
|
+
s = rest;
|
|
298
|
+
}
|
|
299
|
+
return s;
|
|
289
300
|
}
|
|
290
301
|
async function setSession(sessionToken, data) {
|
|
291
302
|
data = dropStaleDiscardMarker(data);
|
|
@@ -54,7 +54,6 @@ export declare const siteEvents: {
|
|
|
54
54
|
}) => void;
|
|
55
55
|
loginFailed: (opts: {
|
|
56
56
|
reason: string;
|
|
57
|
-
email?: string;
|
|
58
57
|
url?: string;
|
|
59
58
|
user_agent?: string;
|
|
60
59
|
ip_address?: string;
|
|
@@ -106,9 +105,7 @@ export declare const siteEvents: {
|
|
|
106
105
|
ip_address?: string;
|
|
107
106
|
}) => void;
|
|
108
107
|
error: (opts: {
|
|
109
|
-
|
|
110
|
-
error?: string;
|
|
111
|
-
stack?: string;
|
|
108
|
+
code: string;
|
|
112
109
|
user_id?: string | number;
|
|
113
110
|
url?: string;
|
|
114
111
|
user_agent?: string;
|
|
@@ -139,7 +136,6 @@ declare const _default: {
|
|
|
139
136
|
}) => void;
|
|
140
137
|
loginFailed: (opts: {
|
|
141
138
|
reason: string;
|
|
142
|
-
email?: string;
|
|
143
139
|
url?: string;
|
|
144
140
|
user_agent?: string;
|
|
145
141
|
ip_address?: string;
|
|
@@ -191,9 +187,7 @@ declare const _default: {
|
|
|
191
187
|
ip_address?: string;
|
|
192
188
|
}) => void;
|
|
193
189
|
error: (opts: {
|
|
194
|
-
|
|
195
|
-
error?: string;
|
|
196
|
-
stack?: string;
|
|
190
|
+
code: string;
|
|
197
191
|
user_id?: string | number;
|
|
198
192
|
url?: string;
|
|
199
193
|
user_agent?: string;
|
package/dist/lib/site-logger.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.configureSiteLogger = configureSiteLogger;
|
|
|
16
16
|
exports.logSiteEvent = logSiteEvent;
|
|
17
17
|
exports.getClientIp = getClientIp;
|
|
18
18
|
const redis_1 = require("./redis");
|
|
19
|
+
const log_safe_1 = require("./log-safe");
|
|
19
20
|
// Redis key for site logs (separate from data_logs)
|
|
20
21
|
const REDIS_SITE_LOG_KEY = 'vibe:site-logs:pending';
|
|
21
22
|
const REDIS_LOG_TTL = 7 * 24 * 60 * 60; // 1 week
|
|
@@ -53,12 +54,12 @@ async function logSiteEvent(entry) {
|
|
|
53
54
|
console.log('[site-logger] Redis push success, queue length:', result);
|
|
54
55
|
redis.expire(REDIS_SITE_LOG_KEY, REDIS_LOG_TTL).catch(() => { });
|
|
55
56
|
}).catch(err => {
|
|
56
|
-
console.error('[site-logger] Redis push failed:',
|
|
57
|
+
console.error('[site-logger] Redis push failed:', (0, log_safe_1.errForLog)(err));
|
|
57
58
|
});
|
|
58
59
|
}
|
|
59
60
|
catch (error) {
|
|
60
61
|
// Fail silently - logging should never break the app
|
|
61
|
-
console.error('[site-logger] Failed to log event:', error);
|
|
62
|
+
console.error('[site-logger] Failed to log event:', (0, log_safe_1.errForLog)(error));
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
/**
|
|
@@ -94,12 +95,14 @@ exports.siteEvents = {
|
|
|
94
95
|
ip_address: opts.ip_address,
|
|
95
96
|
});
|
|
96
97
|
},
|
|
98
|
+
// 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),
|
|
99
|
+
// 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.
|
|
97
100
|
loginFailed: (opts) => {
|
|
98
101
|
logSiteEvent({
|
|
99
102
|
level: 'warn',
|
|
100
103
|
category: 'auth',
|
|
101
104
|
message: 'Login failed',
|
|
102
|
-
context: { reason: opts.reason
|
|
105
|
+
context: { reason: (0, log_safe_1.closedCodeForLog)(opts.reason) ?? 'UNSPECIFIED' },
|
|
103
106
|
url: opts.url || '/auth/login',
|
|
104
107
|
user_agent: opts.user_agent,
|
|
105
108
|
ip_address: opts.ip_address,
|
|
@@ -181,12 +184,15 @@ exports.siteEvents = {
|
|
|
181
184
|
ip_address: opts.ip_address,
|
|
182
185
|
});
|
|
183
186
|
},
|
|
187
|
+
// 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
|
|
188
|
+
// sentence 'Application error'; the code (UPPER_SNAKE, else UNSPECIFIED) goes in context. A caller that still passes `message`, `error` or `stack`
|
|
189
|
+
// fails to compile.
|
|
184
190
|
error: (opts) => {
|
|
185
191
|
logSiteEvent({
|
|
186
192
|
level: 'error',
|
|
187
193
|
category: 'error',
|
|
188
|
-
message:
|
|
189
|
-
context: {
|
|
194
|
+
message: 'Application error',
|
|
195
|
+
context: { code: (0, log_safe_1.closedCodeForLog)(opts.code) ?? 'UNSPECIFIED' },
|
|
190
196
|
user_id: opts.user_id,
|
|
191
197
|
url: opts.url,
|
|
192
198
|
user_agent: opts.user_agent,
|
|
@@ -48,6 +48,7 @@ exports.extractApiItems = extractApiItems;
|
|
|
48
48
|
// NO MORE GUESSING data.data.data.data - EVER AGAIN!
|
|
49
49
|
// ========================================================================================
|
|
50
50
|
const better_auth_client_1 = require("../client/better-auth-client");
|
|
51
|
+
const log_safe_1 = require("./log-safe");
|
|
51
52
|
/** Unwrap Better Auth getSession() to a flat session-like object for backward compat */
|
|
52
53
|
async function getSession() {
|
|
53
54
|
const { data } = await better_auth_client_1.authClient.getSession();
|
|
@@ -172,7 +173,7 @@ function scheduleLoginRedirect(isImmediate = false) {
|
|
|
172
173
|
window.location.href = `/account-auth/login?returnUrl=${returnUrl}`;
|
|
173
174
|
}
|
|
174
175
|
catch (error) {
|
|
175
|
-
console.error('❌ Error during login redirect:', error);
|
|
176
|
+
console.error('❌ Error during login redirect:', (0, log_safe_1.errForLog)(error));
|
|
176
177
|
// Final fallback
|
|
177
178
|
try {
|
|
178
179
|
window.location.href = '/account-auth/login';
|
|
@@ -640,7 +641,7 @@ class StandardizedClientApiService {
|
|
|
640
641
|
throw error;
|
|
641
642
|
}
|
|
642
643
|
// Wrap unknown errors as network errors
|
|
643
|
-
console.error('❌ API request failed:', error);
|
|
644
|
+
console.error('❌ API request failed:', (0, log_safe_1.errForLog)(error));
|
|
644
645
|
throw new ApiNetworkError(error instanceof Error ? error.message : 'Network error', 0, endpoint);
|
|
645
646
|
}
|
|
646
647
|
}
|
|
@@ -28,8 +28,10 @@ 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
|
-
|
|
32
|
-
const
|
|
31
|
+
const refresh_window_1 = require("./refresh-window");
|
|
32
|
+
const log_safe_1 = require("./log-safe");
|
|
33
|
+
// "needs refresh" threshold: THE one freshness window (lib/refresh-window), the same value the refresh route, ensureFreshAccessToken and idp-fetch use.
|
|
34
|
+
const REFRESH_THRESHOLD_MS = refresh_window_1.REFRESH_WINDOW_MS;
|
|
33
35
|
// Concurrent refresh handling configuration
|
|
34
36
|
const CONCURRENT_REFRESH_POLL_INTERVAL_MS = 200; // How often to poll session during concurrent refresh
|
|
35
37
|
const CONCURRENT_REFRESH_MAX_WAIT_MS = 8000; // Max time to wait for concurrent refresh to complete
|
|
@@ -357,7 +359,7 @@ async function ensureFreshToken(request) {
|
|
|
357
359
|
}
|
|
358
360
|
catch (e) {
|
|
359
361
|
// If we can't decode, proceed with normal logic
|
|
360
|
-
console.warn('[TOKEN_LIFECYCLE] Could not validate JWT exp claim:', e);
|
|
362
|
+
console.warn('[TOKEN_LIFECYCLE] Could not validate JWT exp claim:', (0, log_safe_1.errForLog)(e));
|
|
361
363
|
}
|
|
362
364
|
}
|
|
363
365
|
console.log('[TOKEN_LIFECYCLE] ensureFreshToken check:', {
|
|
@@ -428,7 +430,7 @@ async function ensureFreshToken(request) {
|
|
|
428
430
|
};
|
|
429
431
|
}
|
|
430
432
|
catch (error) {
|
|
431
|
-
console.error('[TOKEN_LIFECYCLE] Error:', error);
|
|
433
|
+
console.error('[TOKEN_LIFECYCLE] Error:', (0, log_safe_1.errForLog)(error));
|
|
432
434
|
return {
|
|
433
435
|
success: false,
|
|
434
436
|
error: 'NO_SESSION',
|
|
@@ -31,6 +31,7 @@ const app_slug_1 = require("../lib/app-slug");
|
|
|
31
31
|
const rbac_check_1 = require("./rbac-check");
|
|
32
32
|
const session_cookie_clear_1 = require("../auth/session-cookie-clear");
|
|
33
33
|
const session_reset_link_1 = require("../auth/session-reset-link");
|
|
34
|
+
const log_safe_1 = require("../lib/log-safe");
|
|
34
35
|
// =============================================================================
|
|
35
36
|
// DEFAULT IMPLEMENTATIONS
|
|
36
37
|
// =============================================================================
|
|
@@ -748,7 +749,7 @@ async function handleAllow(request, pathname, sessionPointer, sessionStatus, rba
|
|
|
748
749
|
}
|
|
749
750
|
}
|
|
750
751
|
catch (error) {
|
|
751
|
-
console.error('[MIDDLEWARE] RBAC error:', error);
|
|
752
|
+
console.error('[MIDDLEWARE] RBAC error:', (0, log_safe_1.errForLog)(error));
|
|
752
753
|
// In development, fail open
|
|
753
754
|
if (process.env.NODE_ENV !== 'production') {
|
|
754
755
|
console.warn('[MIDDLEWARE] RBAC: Allowing in development despite error');
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Page RBAC Check Module
|
|
3
|
-
*
|
|
4
|
-
* Checks page-level permissions via Vibe API through the IDP Proxy.
|
|
5
|
-
* Uses in-memory cache to reduce API calls.
|
|
6
|
-
* Fails closed (DENY) on errors or timeout.
|
|
7
|
-
*
|
|
8
|
-
* All requests route through the IDP Vibe Proxy ({IDP_URL}/api/vibe/proxy)
|
|
9
|
-
* which injects proper HMAC credentials for the Vibe API.
|
|
10
|
-
*
|
|
11
|
-
* @version 2.0.0
|
|
12
|
-
* @since page-rbac-2026-01
|
|
13
|
-
*/
|
|
14
1
|
export interface RBACResult {
|
|
15
2
|
allowed: boolean;
|
|
16
3
|
requires_2fa?: boolean;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clearRBACCache = clearRBACCache;
|
|
4
|
+
exports.checkPagePermission = checkPagePermission;
|
|
5
|
+
exports.isRBACEnabled = isRBACEnabled;
|
|
6
|
+
const log_safe_1 = require("../lib/log-safe");
|
|
2
7
|
/**
|
|
3
8
|
* Page RBAC Check Module
|
|
4
9
|
*
|
|
@@ -12,10 +17,6 @@
|
|
|
12
17
|
* @version 2.0.0
|
|
13
18
|
* @since page-rbac-2026-01
|
|
14
19
|
*/
|
|
15
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
-
exports.clearRBACCache = clearRBACCache;
|
|
17
|
-
exports.checkPagePermission = checkPagePermission;
|
|
18
|
-
exports.isRBACEnabled = isRBACEnabled;
|
|
19
20
|
// ============================================================================
|
|
20
21
|
// WEB CRYPTO HELPERS (Edge Runtime compatible)
|
|
21
22
|
// ============================================================================
|
|
@@ -203,7 +204,7 @@ async function checkPagePermission(path, userRoles, clientId, userClaims) {
|
|
|
203
204
|
redirect: '/error?code=rbac_timeout',
|
|
204
205
|
};
|
|
205
206
|
}
|
|
206
|
-
console.error('[RBAC] Proxy error:', error);
|
|
207
|
+
console.error('[RBAC] Proxy error:', (0, log_safe_1.errForLog)(error));
|
|
207
208
|
return {
|
|
208
209
|
allowed: false,
|
|
209
210
|
reason: 'rbac_service_unavailable',
|
|
@@ -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
|
/**
|