@payez/next-mvp 4.5.0 → 4.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/auth-decision.d.ts +10 -1
- package/dist/auth/auth-decision.js +23 -1
- package/dist/edge/middleware.d.ts +1 -1
- package/dist/edge/middleware.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/lib/idp-fetch.d.ts +8 -1
- package/dist/lib/idp-fetch.js +5 -2
- package/dist/lib/retry-after.d.ts +10 -0
- package/dist/lib/retry-after.js +30 -0
- package/dist/lib/session-store.d.ts +17 -0
- package/dist/lib/session-store.js +33 -0
- package/dist/lib/two-factor-verify-failure.d.ts +8 -0
- package/dist/lib/two-factor-verify-failure.js +35 -3
- package/dist/middleware/create-middleware.d.ts +10 -0
- package/dist/middleware/create-middleware.js +30 -1
- package/dist/server/auth.js +87 -3
- package/dist/vibe/sessions.d.ts +10 -22
- package/dist/vibe/sessions.js +159 -24
- package/package.json +1042 -1042
- package/scripts/check-publish-health.mjs +12 -3
- package/scripts/publish-health-tag.mjs +27 -0
- package/src/auth/auth-decision.ts +29 -1
- package/src/edge/middleware.ts +1 -1
- package/src/index.ts +1 -1
- package/src/lib/idp-fetch.ts +10 -2
- package/src/lib/retry-after.ts +24 -0
- package/src/lib/session-store.ts +42 -0
- package/src/lib/two-factor-verify-failure.ts +277 -248
- package/src/middleware/create-middleware.ts +26 -1
- package/src/server/auth.ts +90 -3
- package/src/vibe/document-unwrap.ts +103 -103
- package/src/vibe/sessions.ts +172 -27
|
@@ -21,6 +21,8 @@ export interface AuthContext {
|
|
|
21
21
|
twoFactorComplete: boolean;
|
|
22
22
|
};
|
|
23
23
|
circuitBreakerOpen: boolean;
|
|
24
|
+
/** PAY-2136: the request is a router prefetch. A prefetch never clears cookies (see isPrefetchRequest). */
|
|
25
|
+
isPrefetch?: boolean;
|
|
24
26
|
}
|
|
25
27
|
export type AuthAction = {
|
|
26
28
|
type: 'allow';
|
|
@@ -34,6 +36,13 @@ export type AuthAction = {
|
|
|
34
36
|
reason: string;
|
|
35
37
|
};
|
|
36
38
|
/**
|
|
37
|
-
* Main decision function - pure, testable, deterministic
|
|
39
|
+
* Main decision function - pure, testable, deterministic.
|
|
40
|
+
* PAY-2136: when `context.isPrefetch` is set, no redirect it returns asks for cookies to be cleared; the
|
|
41
|
+
* redirect itself is kept, and the next real navigation reaches the same decision and clears then.
|
|
42
|
+
*
|
|
43
|
+
* @deprecated PAY-2180: this exported engine is NOT the one the middleware runs. `createMvpMiddleware` decides with
|
|
44
|
+
* its own private engine (middleware/create-middleware.ts), so calling this answers a question nothing enforces.
|
|
45
|
+
* A sweep found no code consumer in the portal, idealvibe, idealresume or cryptaply. It will be REMOVED in the next
|
|
46
|
+
* major version, together with its re-exports from the package root and `/edge`. Use `createMvpMiddleware`.
|
|
38
47
|
*/
|
|
39
48
|
export declare function makeAuthDecision(context: AuthContext): AuthAction;
|
|
@@ -9,10 +9,32 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
9
9
|
exports.makeAuthDecision = makeAuthDecision;
|
|
10
10
|
const safe_callback_1 = require("./safe-callback");
|
|
11
11
|
const token_log_id_1 = require("../lib/token-log-id");
|
|
12
|
+
/** PAY-2180: warn once per process, not per request. */
|
|
13
|
+
let deprecationWarned = false;
|
|
12
14
|
/**
|
|
13
|
-
* Main decision function - pure, testable, deterministic
|
|
15
|
+
* Main decision function - pure, testable, deterministic.
|
|
16
|
+
* PAY-2136: when `context.isPrefetch` is set, no redirect it returns asks for cookies to be cleared; the
|
|
17
|
+
* redirect itself is kept, and the next real navigation reaches the same decision and clears then.
|
|
18
|
+
*
|
|
19
|
+
* @deprecated PAY-2180: this exported engine is NOT the one the middleware runs. `createMvpMiddleware` decides with
|
|
20
|
+
* its own private engine (middleware/create-middleware.ts), so calling this answers a question nothing enforces.
|
|
21
|
+
* A sweep found no code consumer in the portal, idealvibe, idealresume or cryptaply. It will be REMOVED in the next
|
|
22
|
+
* major version, together with its re-exports from the package root and `/edge`. Use `createMvpMiddleware`.
|
|
14
23
|
*/
|
|
15
24
|
function makeAuthDecision(context) {
|
|
25
|
+
if (!deprecationWarned) {
|
|
26
|
+
deprecationWarned = true;
|
|
27
|
+
console.warn('[next-mvp] makeAuthDecision (auth/auth-decision) is deprecated (PAY-2180) and will be removed in the next major: ' +
|
|
28
|
+
'createMvpMiddleware does not use it. Use createMvpMiddleware.');
|
|
29
|
+
}
|
|
30
|
+
const action = decide(context);
|
|
31
|
+
if (context.isPrefetch && action.type === 'redirect' && action.clearCookies) {
|
|
32
|
+
const { clearCookies: _dropped, ...rest } = action;
|
|
33
|
+
return rest;
|
|
34
|
+
}
|
|
35
|
+
return action;
|
|
36
|
+
}
|
|
37
|
+
function decide(context) {
|
|
16
38
|
const { pathname, isPublicRoute, isLoginPage, searchParams, sessionPointer, sessionStatus, circuitBreakerOpen } = context;
|
|
17
39
|
// SAFEGUARD: Never use auth pages as callback URLs to prevent redirect loops.
|
|
18
40
|
// 375656: `pathname` is our own request path rather than an attacker-supplied
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
export { makeAuthDecision } from '../auth/auth-decision';
|
|
8
8
|
export { isUnauthenticatedRoute, configurePublicRoutes, getRouteConfig, configure2FABypassRoutes, should2FABypass, get2FABypassConfig } from '../auth/route-config';
|
|
9
9
|
export type { UnauthenticatedRouteConfig, ProtectedRouteConfig } from '../auth/route-config';
|
|
10
|
-
export { createMvpMiddleware } from '../middleware/create-middleware';
|
|
10
|
+
export { createMvpMiddleware, isPrefetchRequest } from '../middleware/create-middleware';
|
|
11
11
|
export type { MvpMiddlewareOptions, CircuitBreakerProvider, MiddlewareLogger, AuthContext, AuthAction, SessionPointer, SessionStatus } from '../middleware/create-middleware';
|
|
12
12
|
export { TwoFactorPresets, AMRValues, ACRLevels, validateAMR, validateACR, checkTwoFactorRequirements } from '../middleware/twofa-presets';
|
|
13
13
|
export type { TwoFactorRequirements, RouteConfig } from '../middleware/twofa-presets';
|
package/dist/edge/middleware.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Client-side utilities that depend on browser APIs or server-only code are excluded.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.getInternalApiUrl = exports.checkTwoFactorRequirements = exports.validateACR = exports.validateAMR = exports.ACRLevels = exports.AMRValues = exports.TwoFactorPresets = exports.createMvpMiddleware = exports.get2FABypassConfig = exports.should2FABypass = exports.configure2FABypassRoutes = exports.getRouteConfig = exports.configurePublicRoutes = exports.isUnauthenticatedRoute = exports.makeAuthDecision = void 0;
|
|
9
|
+
exports.getInternalApiUrl = exports.checkTwoFactorRequirements = exports.validateACR = exports.validateAMR = exports.ACRLevels = exports.AMRValues = exports.TwoFactorPresets = exports.isPrefetchRequest = exports.createMvpMiddleware = exports.get2FABypassConfig = exports.should2FABypass = exports.configure2FABypassRoutes = exports.getRouteConfig = exports.configurePublicRoutes = exports.isUnauthenticatedRoute = exports.makeAuthDecision = void 0;
|
|
10
10
|
// Authentication middleware and configuration (Edge Runtime compatible)
|
|
11
11
|
var auth_decision_1 = require("../auth/auth-decision");
|
|
12
12
|
Object.defineProperty(exports, "makeAuthDecision", { enumerable: true, get: function () { return auth_decision_1.makeAuthDecision; } });
|
|
@@ -19,6 +19,7 @@ Object.defineProperty(exports, "should2FABypass", { enumerable: true, get: funct
|
|
|
19
19
|
Object.defineProperty(exports, "get2FABypassConfig", { enumerable: true, get: function () { return route_config_1.get2FABypassConfig; } });
|
|
20
20
|
var create_middleware_1 = require("../middleware/create-middleware");
|
|
21
21
|
Object.defineProperty(exports, "createMvpMiddleware", { enumerable: true, get: function () { return create_middleware_1.createMvpMiddleware; } });
|
|
22
|
+
Object.defineProperty(exports, "isPrefetchRequest", { enumerable: true, get: function () { return create_middleware_1.isPrefetchRequest; } });
|
|
22
23
|
// Two-Factor Authentication Presets (Edge Runtime compatible)
|
|
23
24
|
var twofa_presets_1 = require("../middleware/twofa-presets");
|
|
24
25
|
Object.defineProperty(exports, "TwoFactorPresets", { enumerable: true, get: function () { return twofa_presets_1.TwoFactorPresets; } });
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type { AuthConfig } from './types/auth';
|
|
|
6
6
|
export { makeAuthDecision } from './auth/auth-decision';
|
|
7
7
|
export { isUnauthenticatedRoute, configurePublicRoutes, getRouteConfig } from './auth/route-config';
|
|
8
8
|
export { safeCallbackPath, isSafeCallbackPath, resolveCallbackDestination, DEFAULT_CALLBACK } from './auth/safe-callback';
|
|
9
|
-
export { createMvpMiddleware } from './middleware/create-middleware';
|
|
9
|
+
export { createMvpMiddleware, isPrefetchRequest } from './middleware/create-middleware';
|
|
10
10
|
export { UserAvatarMenu, MobileNavDrawer } from './components/account';
|
|
11
11
|
export type { UserAvatarMenuProps, MobileNavDrawerProps, NavItem, NavSection } from './components/account';
|
|
12
12
|
export { ErrorMetricsCard, HealthMetricsCard, AuditLogViewer, AdminAnalyticsLayout, useErrorMetrics, useHealthMetrics, useAuditLog, useAdminAnalytics, getErrorMetrics, getHealthMetrics, writeAuditLog, queryAuditLog, } from './logging';
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// - Better Auth: import { createBetterAuthInstance } from '@payez/next-mvp/auth/better-auth'
|
|
9
9
|
// - Server auth: import { getSession } from '@payez/next-mvp/server/auth'
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.queryAuditLog = exports.writeAuditLog = exports.getHealthMetrics = exports.getErrorMetrics = exports.useAdminAnalytics = exports.useAuditLog = exports.useHealthMetrics = exports.useErrorMetrics = exports.AdminAnalyticsLayout = exports.AuditLogViewer = exports.HealthMetricsCard = exports.ErrorMetricsCard = exports.MobileNavDrawer = exports.UserAvatarMenu = exports.createMvpMiddleware = exports.DEFAULT_CALLBACK = exports.resolveCallbackDestination = exports.isSafeCallbackPath = exports.safeCallbackPath = exports.getRouteConfig = exports.configurePublicRoutes = exports.isUnauthenticatedRoute = exports.makeAuthDecision = exports.useTraditionalAuthEnabled = exports.useFederatedAuthEnabled = exports.useFederatedProviders = exports.useAuthMode = exports.useAuthConfig = exports.AuthProvider = exports.useAnonSession = exports.fetchWithAuth = void 0;
|
|
11
|
+
exports.queryAuditLog = exports.writeAuditLog = exports.getHealthMetrics = exports.getErrorMetrics = exports.useAdminAnalytics = exports.useAuditLog = exports.useHealthMetrics = exports.useErrorMetrics = exports.AdminAnalyticsLayout = exports.AuditLogViewer = exports.HealthMetricsCard = exports.ErrorMetricsCard = exports.MobileNavDrawer = exports.UserAvatarMenu = exports.isPrefetchRequest = exports.createMvpMiddleware = exports.DEFAULT_CALLBACK = exports.resolveCallbackDestination = exports.isSafeCallbackPath = exports.safeCallbackPath = exports.getRouteConfig = exports.configurePublicRoutes = exports.isUnauthenticatedRoute = exports.makeAuthDecision = exports.useTraditionalAuthEnabled = exports.useFederatedAuthEnabled = exports.useFederatedProviders = exports.useAuthMode = exports.useAuthConfig = exports.AuthProvider = exports.useAnonSession = exports.fetchWithAuth = void 0;
|
|
12
12
|
// Client-safe exports only
|
|
13
13
|
// Client-side utilities
|
|
14
14
|
var fetch_with_auth_1 = require("./client/fetch-with-auth");
|
|
@@ -40,6 +40,7 @@ Object.defineProperty(exports, "resolveCallbackDestination", { enumerable: true,
|
|
|
40
40
|
Object.defineProperty(exports, "DEFAULT_CALLBACK", { enumerable: true, get: function () { return safe_callback_1.DEFAULT_CALLBACK; } });
|
|
41
41
|
var create_middleware_1 = require("./middleware/create-middleware");
|
|
42
42
|
Object.defineProperty(exports, "createMvpMiddleware", { enumerable: true, get: function () { return create_middleware_1.createMvpMiddleware; } });
|
|
43
|
+
Object.defineProperty(exports, "isPrefetchRequest", { enumerable: true, get: function () { return create_middleware_1.isPrefetchRequest; } });
|
|
43
44
|
// Account Components
|
|
44
45
|
var account_1 = require("./components/account");
|
|
45
46
|
Object.defineProperty(exports, "UserAvatarMenu", { enumerable: true, get: function () { return account_1.UserAvatarMenu; } });
|
package/dist/lib/idp-fetch.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server';
|
|
2
2
|
import { assertAbsoluteIdpUrl } from './idp-url-guard';
|
|
3
|
-
|
|
3
|
+
import { parseRetryAfterSeconds } from './retry-after';
|
|
4
|
+
export { assertAbsoluteIdpUrl, parseRetryAfterSeconds };
|
|
4
5
|
/**
|
|
5
6
|
* Centralized IDP fetch helper
|
|
6
7
|
* - Injects Bearer from Redis session
|
|
@@ -12,5 +13,11 @@ export interface IdpFetchResult<T = any> {
|
|
|
12
13
|
status: number;
|
|
13
14
|
json: T | null;
|
|
14
15
|
attemptedRefresh: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* PAY-1608: the upstream Retry-After in whole seconds, when the IDP sent a usable one. Core sends it on every
|
|
18
|
+
* 2FA lock (MfaController.SecondFactorLocked: 429 + Retry-After 900; VerifyTotp: what is left of the lock).
|
|
19
|
+
* It used to be dropped here, so no BFF could state the real lock window. Absent otherwise.
|
|
20
|
+
*/
|
|
21
|
+
retryAfterSeconds?: number;
|
|
15
22
|
}
|
|
16
23
|
export declare function idpFetchJSON<T = any>(req: NextRequest, targetUrl: string, init?: RequestInit): Promise<IdpFetchResult<T>>;
|
package/dist/lib/idp-fetch.js
CHANGED
|
@@ -33,13 +33,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.assertAbsoluteIdpUrl = void 0;
|
|
36
|
+
exports.parseRetryAfterSeconds = exports.assertAbsoluteIdpUrl = void 0;
|
|
37
37
|
exports.idpFetchJSON = idpFetchJSON;
|
|
38
38
|
const test_aware_get_token_1 = require("./test-aware-get-token");
|
|
39
39
|
const session_store_1 = require("./session-store");
|
|
40
40
|
const internal_api_1 = require("./internal-api");
|
|
41
41
|
const idp_url_guard_1 = require("./idp-url-guard");
|
|
42
42
|
Object.defineProperty(exports, "assertAbsoluteIdpUrl", { enumerable: true, get: function () { return idp_url_guard_1.assertAbsoluteIdpUrl; } });
|
|
43
|
+
const retry_after_1 = require("./retry-after");
|
|
44
|
+
Object.defineProperty(exports, "parseRetryAfterSeconds", { enumerable: true, get: function () { return retry_after_1.parseRetryAfterSeconds; } });
|
|
43
45
|
function buildHeaders(req, bearer, extra) {
|
|
44
46
|
const headers = {
|
|
45
47
|
Accept: 'application/json',
|
|
@@ -177,5 +179,6 @@ async function idpFetchJSON(req, targetUrl, init = {}) {
|
|
|
177
179
|
}
|
|
178
180
|
catch { }
|
|
179
181
|
}
|
|
180
|
-
|
|
182
|
+
const retryAfterSeconds = (0, retry_after_1.parseRetryAfterSeconds)(res.headers?.get?.('Retry-After'));
|
|
183
|
+
return { ok: res.ok, status: res.status, json: json, attemptedRefresh, ...(retryAfterSeconds ? { retryAfterSeconds } : {}) };
|
|
181
184
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAY-1608: parse an HTTP Retry-After header to whole seconds.
|
|
3
|
+
*
|
|
4
|
+
* Accepts delta-seconds ("900") and an HTTP date in the future. Answers undefined for absent, zero, negative,
|
|
5
|
+
* fractional, past or malformed values - a caller that gets undefined states no window rather than a guessed one.
|
|
6
|
+
*
|
|
7
|
+
* A dependency-free leaf on purpose: idp-fetch re-exports it, but a test can load it without the session/redis
|
|
8
|
+
* graph idp-fetch pulls in.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseRetryAfterSeconds(value: string | null | undefined, nowMs?: number): number | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseRetryAfterSeconds = parseRetryAfterSeconds;
|
|
4
|
+
/**
|
|
5
|
+
* PAY-1608: parse an HTTP Retry-After header to whole seconds.
|
|
6
|
+
*
|
|
7
|
+
* Accepts delta-seconds ("900") and an HTTP date in the future. Answers undefined for absent, zero, negative,
|
|
8
|
+
* fractional, past or malformed values - a caller that gets undefined states no window rather than a guessed one.
|
|
9
|
+
*
|
|
10
|
+
* A dependency-free leaf on purpose: idp-fetch re-exports it, but a test can load it without the session/redis
|
|
11
|
+
* graph idp-fetch pulls in.
|
|
12
|
+
*/
|
|
13
|
+
function parseRetryAfterSeconds(value, nowMs = Date.now()) {
|
|
14
|
+
if (typeof value !== 'string')
|
|
15
|
+
return undefined;
|
|
16
|
+
const v = value.trim();
|
|
17
|
+
if (/^\d+$/.test(v)) {
|
|
18
|
+
const n = parseInt(v, 10);
|
|
19
|
+
return n > 0 ? n : undefined;
|
|
20
|
+
}
|
|
21
|
+
// Only something that looks like an HTTP date (it has letters: "Sat, 10 Oct 2026 12:01:30 GMT") is parsed as
|
|
22
|
+
// one; "-5" and "1.5" must not reach Date.parse, which would read them as years.
|
|
23
|
+
if (!/[a-z]/i.test(v))
|
|
24
|
+
return undefined;
|
|
25
|
+
const at = Date.parse(v);
|
|
26
|
+
if (!Number.isFinite(at))
|
|
27
|
+
return undefined;
|
|
28
|
+
const secs = Math.ceil((at - nowMs) / 1000);
|
|
29
|
+
return secs > 0 ? secs : undefined;
|
|
30
|
+
}
|
|
@@ -218,3 +218,20 @@ export declare function isRefreshInProgress(sessionToken: string): Promise<boole
|
|
|
218
218
|
* Force cleanup of expired or orphaned refresh locks
|
|
219
219
|
*/
|
|
220
220
|
export declare function cleanupRefreshLocks(): Promise<number>;
|
|
221
|
+
/**
|
|
222
|
+
* PAY-2136 DIAGNOSTIC (BAPert 70873): which of the session keys EXIST for a token, right now.
|
|
223
|
+
*
|
|
224
|
+
* Names/presence only - this returns booleans and the key PREFIX (an app slug, not a secret), never a value and
|
|
225
|
+
* never any part of the token. It exists so a NO_SESSION can be read for what it was: canonical row gone with the
|
|
226
|
+
* ba: record alive (the :330 asymmetry), both gone (expired/deleted), or the store could not be asked ('error').
|
|
227
|
+
*
|
|
228
|
+
* EXISTS, not GET: it cannot parse, cannot throw on a malformed blob, and cannot return a credential. Each key is
|
|
229
|
+
* asked separately so one timeout does not hide the other two. Never throws.
|
|
230
|
+
*/
|
|
231
|
+
export type KeyPresence = boolean | 'error';
|
|
232
|
+
export declare function describeSessionKeyPresence(sessionToken: string): Promise<{
|
|
233
|
+
prefix: string;
|
|
234
|
+
canonical: KeyPresence;
|
|
235
|
+
ba: KeyPresence;
|
|
236
|
+
ver: KeyPresence;
|
|
237
|
+
}>;
|
|
@@ -37,6 +37,7 @@ exports.releaseRefreshLock = releaseRefreshLock;
|
|
|
37
37
|
exports.checkRefreshLock = checkRefreshLock;
|
|
38
38
|
exports.isRefreshInProgress = isRefreshInProgress;
|
|
39
39
|
exports.cleanupRefreshLocks = cleanupRefreshLocks;
|
|
40
|
+
exports.describeSessionKeyPresence = describeSessionKeyPresence;
|
|
40
41
|
const redis_1 = __importDefault(require("./redis"));
|
|
41
42
|
const crypto_1 = require("crypto");
|
|
42
43
|
const app_slug_1 = require("./app-slug");
|
|
@@ -927,3 +928,35 @@ async function cleanupRefreshLocks() {
|
|
|
927
928
|
console.warn('[SESSION-STORE] cleanupRefreshLocks called - scanning for expired locks');
|
|
928
929
|
return 0;
|
|
929
930
|
}
|
|
931
|
+
async function describeSessionKeyPresence(sessionToken) {
|
|
932
|
+
const ask = async (key) => {
|
|
933
|
+
try {
|
|
934
|
+
return (await redis_1.default.exists(key)) > 0;
|
|
935
|
+
}
|
|
936
|
+
catch {
|
|
937
|
+
return 'error';
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
let prefix = '';
|
|
941
|
+
try {
|
|
942
|
+
prefix = (0, app_slug_1.getSessionPrefix)();
|
|
943
|
+
}
|
|
944
|
+
catch {
|
|
945
|
+
prefix = '';
|
|
946
|
+
}
|
|
947
|
+
if (!sessionToken)
|
|
948
|
+
return { prefix, canonical: false, ba: false, ver: false };
|
|
949
|
+
let canonicalKey = '';
|
|
950
|
+
let baKey = '';
|
|
951
|
+
let verKey = '';
|
|
952
|
+
try {
|
|
953
|
+
canonicalKey = getSessionKey(sessionToken);
|
|
954
|
+
baKey = getBetterAuthSessionKey(sessionToken, (0, app_slug_1.getAppSlug)());
|
|
955
|
+
verKey = getSessionVersionKey(sessionToken);
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
return { prefix, canonical: 'error', ba: 'error', ver: 'error' };
|
|
959
|
+
}
|
|
960
|
+
const [canonical, ba, ver] = await Promise.all([ask(canonicalKey), ask(baKey), ask(verKey)]);
|
|
961
|
+
return { prefix, canonical, ba, ver };
|
|
962
|
+
}
|
|
@@ -56,10 +56,18 @@ export declare function stepUpRefusal(status: number, json: unknown): StepUpRefu
|
|
|
56
56
|
*/
|
|
57
57
|
export declare function upstreamErrorTexts(json: unknown): string[];
|
|
58
58
|
export declare function classifyTwoFactorVerifyFailure(status: number, json: unknown): TwoFactorVerifyFailureCode;
|
|
59
|
+
/**
|
|
60
|
+
* PAY-1608: the window as words - "15 minutes", "1 minute", "45 seconds", "2 hours". Rounded UP, so the user is never
|
|
61
|
+
* told a shorter wait than the IDP enforces. Undefined in, undefined out.
|
|
62
|
+
*/
|
|
63
|
+
export declare function formatLockWindow(seconds: number | undefined): string | undefined;
|
|
64
|
+
/** PAY-1608: the LOCKED sentence with the IDP's real window, or the number-free one when none was sent. */
|
|
65
|
+
export declare function lockedText(retryAfterSeconds?: number): string;
|
|
59
66
|
export declare function twoFactorVerifyFailure(result: {
|
|
60
67
|
status: number;
|
|
61
68
|
json: unknown;
|
|
62
69
|
attemptedRefresh: boolean;
|
|
70
|
+
retryAfterSeconds?: number;
|
|
63
71
|
}, method: TwoFactorVerifyMethod): NextResponse;
|
|
64
72
|
/**
|
|
65
73
|
* PAY-1443 residual: account/send-code on the SAME verify-code screen. A send has no
|
|
@@ -4,6 +4,8 @@ exports.STEP_UP_REFUSAL_CODE = void 0;
|
|
|
4
4
|
exports.stepUpRefusal = stepUpRefusal;
|
|
5
5
|
exports.upstreamErrorTexts = upstreamErrorTexts;
|
|
6
6
|
exports.classifyTwoFactorVerifyFailure = classifyTwoFactorVerifyFailure;
|
|
7
|
+
exports.formatLockWindow = formatLockWindow;
|
|
8
|
+
exports.lockedText = lockedText;
|
|
7
9
|
exports.twoFactorVerifyFailure = twoFactorVerifyFailure;
|
|
8
10
|
exports.classifyTwoFactorSendFailure = classifyTwoFactorSendFailure;
|
|
9
11
|
exports.twoFactorSendCodeFailure = twoFactorSendCodeFailure;
|
|
@@ -33,7 +35,9 @@ const INVALID_CODE_TEXT = {
|
|
|
33
35
|
backup_code: 'Invalid backup code. Please try again.',
|
|
34
36
|
};
|
|
35
37
|
const FIXED_TEXT = {
|
|
36
|
-
|
|
38
|
+
// PAY-1608: NO NUMBER HERE. This is the sentence for a lock whose window the IDP did not send; lockedText()
|
|
39
|
+
// states the real window when Retry-After arrives. A hard-coded "15 minutes" was true only while every lock was 15.
|
|
40
|
+
LOCKED: 'Too many incorrect codes. Verification is locked for now - even a correct code is refused until the lock ends. Wait before entering another code.',
|
|
37
41
|
RATE_LIMITED: 'Too many attempts. Please wait a moment and try again.',
|
|
38
42
|
SESSION_REJECTED: 'We could not verify the code for this session. Please try again.',
|
|
39
43
|
VERIFY_UNAVAILABLE: 'We could not verify the code right now. Please try again.',
|
|
@@ -186,13 +190,41 @@ function classifyTwoFactorVerifyFailure(status, json) {
|
|
|
186
190
|
return 'VERIFY_UNAVAILABLE';
|
|
187
191
|
return 'VERIFY_FAILED';
|
|
188
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* PAY-1608: the window as words - "15 minutes", "1 minute", "45 seconds", "2 hours". Rounded UP, so the user is never
|
|
195
|
+
* told a shorter wait than the IDP enforces. Undefined in, undefined out.
|
|
196
|
+
*/
|
|
197
|
+
function formatLockWindow(seconds) {
|
|
198
|
+
if (typeof seconds !== 'number' || !Number.isFinite(seconds) || seconds <= 0)
|
|
199
|
+
return undefined;
|
|
200
|
+
const plural = (n, unit) => `${n} ${unit}${n === 1 ? '' : 's'}`;
|
|
201
|
+
if (seconds < 60)
|
|
202
|
+
return plural(Math.ceil(seconds), 'second');
|
|
203
|
+
const minutes = Math.ceil(seconds / 60);
|
|
204
|
+
if (minutes < 120)
|
|
205
|
+
return plural(minutes, 'minute');
|
|
206
|
+
return plural(Math.ceil(minutes / 60), 'hour');
|
|
207
|
+
}
|
|
208
|
+
/** PAY-1608: the LOCKED sentence with the IDP's real window, or the number-free one when none was sent. */
|
|
209
|
+
function lockedText(retryAfterSeconds) {
|
|
210
|
+
const window = formatLockWindow(retryAfterSeconds);
|
|
211
|
+
return window
|
|
212
|
+
? `Too many incorrect codes. Verification is locked for ${window} - wait before entering another code.`
|
|
213
|
+
: FIXED_TEXT.LOCKED;
|
|
214
|
+
}
|
|
189
215
|
function twoFactorVerifyFailure(result, method) {
|
|
190
216
|
const refusal = stepUpRefusal(result.status, result.json);
|
|
191
217
|
if (refusal)
|
|
192
218
|
return stepUpResponse(refusal, result.attemptedRefresh);
|
|
193
219
|
const code = classifyTwoFactorVerifyFailure(result.status, result.json);
|
|
194
|
-
const error = code === 'INVALID_CODE' ? INVALID_CODE_TEXT[method]
|
|
195
|
-
|
|
220
|
+
const error = code === 'INVALID_CODE' ? INVALID_CODE_TEXT[method]
|
|
221
|
+
: code === 'LOCKED' ? lockedText(result.retryAfterSeconds)
|
|
222
|
+
: FIXED_TEXT[code];
|
|
223
|
+
// PAY-1608: a lock or a throttle relays the IDP's window, as a number (for a client that words it itself) and as the
|
|
224
|
+
// standard header. Never on other failures, and never invented: absent upstream means absent here.
|
|
225
|
+
const wait = (code === 'LOCKED' || code === 'RATE_LIMITED') && formatLockWindow(result.retryAfterSeconds)
|
|
226
|
+
? Math.ceil(result.retryAfterSeconds) : undefined;
|
|
227
|
+
return server_1.NextResponse.json({ success: false, error, code, ...(wait ? { retryAfterSeconds: wait } : {}), meta: { attemptedRefresh: result.attemptedRefresh } }, { status: result.status, ...(wait ? { headers: { 'Retry-After': String(wait) } } : {}) });
|
|
196
228
|
}
|
|
197
229
|
const SEND_LABEL = { sms: 'text message', email: 'email' };
|
|
198
230
|
function classifyTwoFactorSendFailure(status, json) {
|
|
@@ -124,6 +124,16 @@ export interface MvpMiddlewareOptions {
|
|
|
124
124
|
*/
|
|
125
125
|
revocationFailMode?: 'closed' | 'open';
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* PAY-2136 Rule A: a speculative request (router prefetch) must NEVER mutate cookies.
|
|
129
|
+
* A prefetch of /account/security was answered 307 + Set-Cookie (session cookies expired), the browser
|
|
130
|
+
* applied it, and the user's next real request (e.g. Approve) was signed out. Detected by any of:
|
|
131
|
+
* `Next-Router-Prefetch: 1|2` (Next sets it on EVERY Link prefetch; 2 = runtime prefetch) or `Sec-Purpose`/`Purpose`
|
|
132
|
+
* containing "prefetch".
|
|
133
|
+
* NOT `RSC: 1` + `_rsc`: a real client-side navigation carries both too (Nextpert-Scout 70976), so that shape
|
|
134
|
+
* would swallow in-app navigations and stop the middleware refreshing/clearing for them.
|
|
135
|
+
*/
|
|
136
|
+
export declare function isPrefetchRequest(request: NextRequest): boolean;
|
|
127
137
|
/** Login error code for a pre-2FA session that expired before the code was verified (PAY-884). */
|
|
128
138
|
export declare const TWO_FACTOR_TIMEOUT_ERROR = "TwoFactorTimeout";
|
|
129
139
|
export declare function createMvpMiddleware(options?: MvpMiddlewareOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
21
|
exports.TWO_FACTOR_TIMEOUT_ERROR = void 0;
|
|
22
|
+
exports.isPrefetchRequest = isPrefetchRequest;
|
|
22
23
|
exports.createMvpMiddleware = createMvpMiddleware;
|
|
23
24
|
exports.decideForceInvalid = decideForceInvalid;
|
|
24
25
|
const server_1 = require("next/server");
|
|
@@ -78,8 +79,32 @@ function isLoginPage(pathname) {
|
|
|
78
79
|
function getSafeCallbackUrl(pathname, searchParams) {
|
|
79
80
|
return (0, safe_callback_1.resolveCallbackDestination)(pathname, searchParams);
|
|
80
81
|
}
|
|
81
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* PAY-2136 Rule A: a speculative request (router prefetch) must NEVER mutate cookies.
|
|
84
|
+
* A prefetch of /account/security was answered 307 + Set-Cookie (session cookies expired), the browser
|
|
85
|
+
* applied it, and the user's next real request (e.g. Approve) was signed out. Detected by any of:
|
|
86
|
+
* `Next-Router-Prefetch: 1|2` (Next sets it on EVERY Link prefetch; 2 = runtime prefetch) or `Sec-Purpose`/`Purpose`
|
|
87
|
+
* containing "prefetch".
|
|
88
|
+
* NOT `RSC: 1` + `_rsc`: a real client-side navigation carries both too (Nextpert-Scout 70976), so that shape
|
|
89
|
+
* would swallow in-app navigations and stop the middleware refreshing/clearing for them.
|
|
90
|
+
*/
|
|
91
|
+
function isPrefetchRequest(request) {
|
|
92
|
+
const h = request.headers;
|
|
93
|
+
const nrp = h.get('next-router-prefetch');
|
|
94
|
+
if (nrp === '1' || nrp === '2')
|
|
95
|
+
return true;
|
|
96
|
+
if (/prefetch/i.test(h.get('sec-purpose') ?? '') || /prefetch/i.test(h.get('purpose') ?? ''))
|
|
97
|
+
return true;
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Clear all session cookies on a response. The ONE site that writes the expiry Set-Cookie for the middleware,
|
|
102
|
+
* so the prefetch guard lives here: on a prefetch the redirect is still returned (routing unchanged) but
|
|
103
|
+
* carries no Set-Cookie; the next real navigation reaches the same decision and clears then.
|
|
104
|
+
*/
|
|
82
105
|
function clearSessionCookies(response, request) {
|
|
106
|
+
if (isPrefetchRequest(request))
|
|
107
|
+
return;
|
|
83
108
|
const sessionCookie = (0, app_slug_1.getSessionCookieName)();
|
|
84
109
|
const secureCookie = (0, app_slug_1.getSecureSessionCookieName)();
|
|
85
110
|
// Clear main cookies
|
|
@@ -546,6 +571,10 @@ async function executeDecision(request, decision, pathname, sessionPointer, sess
|
|
|
546
571
|
case 'service_error':
|
|
547
572
|
return server_1.NextResponse.redirect(new URL('/service-unavailable', request.url));
|
|
548
573
|
case 'refresh_needed':
|
|
574
|
+
// PAY-2136: a speculative request must not spend (rotate) a single-use refresh token or, on a 401/403,
|
|
575
|
+
// clear the session. Let it through; the real navigation refreshes.
|
|
576
|
+
if (isPrefetchRequest(request))
|
|
577
|
+
return server_1.NextResponse.next();
|
|
549
578
|
return handleRefresh(request, safeCallback, opts);
|
|
550
579
|
}
|
|
551
580
|
}
|
package/dist/server/auth.js
CHANGED
|
@@ -48,6 +48,7 @@ exports.requireSession = requireSession;
|
|
|
48
48
|
require("server-only");
|
|
49
49
|
const better_auth_1 = require("../auth/better-auth");
|
|
50
50
|
const session_store_1 = require("../lib/session-store");
|
|
51
|
+
const token_log_id_1 = require("../lib/token-log-id");
|
|
51
52
|
const verified_session_token_1 = require("./verified-session-token");
|
|
52
53
|
function buildSessionDataFromAuthSession(session) {
|
|
53
54
|
const user = session?.user;
|
|
@@ -138,6 +139,79 @@ function attachSessionData(session, sessionData, sessionToken) {
|
|
|
138
139
|
async function getAuthInstance() {
|
|
139
140
|
return (0, better_auth_1.getBetterAuthInstance)();
|
|
140
141
|
}
|
|
142
|
+
/* ----------------------------------------------------------------------------------------------------------------
|
|
143
|
+
* PAY-2136 DIAGNOSTICS (BAPert 70873, Nextpert-Scout 70872/70875) - LOG ONLY, NO BEHAVIOUR CHANGE.
|
|
144
|
+
*
|
|
145
|
+
* A getFreshIdpToken NO_SESSION used to carry no origin: three different returns produced the same
|
|
146
|
+
* `{error:'NO_SESSION'}`, and getSession() swallowed every Redis error into a bare `catch {}`. These helpers name
|
|
147
|
+
* the origin and make a swallowed read audible. Hard rules: every helper is wrapped so it can NEVER throw or change
|
|
148
|
+
* a return value; nothing here ever logs a token, a cookie VALUE or a Redis value - only a tokenLogId() fingerprint,
|
|
149
|
+
* cookie NAMES, booleans, and an error name/message with any long opaque run redacted.
|
|
150
|
+
* -------------------------------------------------------------------------------------------------------------- */
|
|
151
|
+
/** An error name/message, first 80 chars, with any 24+ char opaque run (a token, a key) replaced. */
|
|
152
|
+
function scrubForLog(v) {
|
|
153
|
+
const raw = v instanceof Error ? v.message : String(v ?? '');
|
|
154
|
+
return raw.replace(/[A-Za-z0-9_\-.+/=]{24,}/g, '[redacted]').slice(0, 80);
|
|
155
|
+
}
|
|
156
|
+
/** `catch {}` sites on the session read path: still swallowed, now audible. */
|
|
157
|
+
function logSessionReadSwallowed(path, err) {
|
|
158
|
+
try {
|
|
159
|
+
const name = scrubForLog(err instanceof Error ? err.name : typeof err);
|
|
160
|
+
console.warn(`[server-auth] SESSION_READ_SWALLOWED path=${path} err=${name} msg=${scrubForLog(err)}`);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
/* a diagnostic must never change the outcome */
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Origin a: getSession(request) produced no token. Cookie NAMES only, never a value. */
|
|
167
|
+
function logNoSessionOriginA(request) {
|
|
168
|
+
try {
|
|
169
|
+
const header = request?.headers?.get('cookie') ?? '';
|
|
170
|
+
// Only segments of the form name=value contribute a NAME. A segment with no '=' is not a cookie pair, and
|
|
171
|
+
// logging it whole could emit a bare token (Nextpert-Scout 70901 finding 1).
|
|
172
|
+
const cookieNames = header
|
|
173
|
+
.split(';')
|
|
174
|
+
// Structural, alphabet-independent (Nextpert-Scout 70945): base64 padding can only sit at the END of a bare
|
|
175
|
+
// value, so a bare padded token splits as name=<token>, value='' or '='. A real pair has a non-empty value that
|
|
176
|
+
// is not only '=' characters. (The old includes('=') let 74% of base64url tokens through as a "name".)
|
|
177
|
+
.filter((c) => {
|
|
178
|
+
const i = c.indexOf('=');
|
|
179
|
+
if (i < 0)
|
|
180
|
+
return false;
|
|
181
|
+
const v = c.slice(i + 1);
|
|
182
|
+
return v !== '' && !/^=+$/.test(v);
|
|
183
|
+
})
|
|
184
|
+
.map((c) => c.split('=')[0].trim())
|
|
185
|
+
.filter((n) => n !== '')
|
|
186
|
+
// A bare token that merely ENDS in base64 padding ('=') still passes the filter above and its "name" would be
|
|
187
|
+
// the token itself (Nextpert-Scout 70913). Real cookie names carry a separator ('.', '-', '_'); a long name with
|
|
188
|
+
// none is opaque, so it is logged as a placeholder, never as itself.
|
|
189
|
+
.map((n) => (n.length >= 24 && !/[._-]/.test(n) ? '[opaque-name]' : n))
|
|
190
|
+
.slice(0, 20);
|
|
191
|
+
console.warn('[server-auth] NO_SESSION origin=a', { hasCookieHeader: header !== '', cookieNames });
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
/* a diagnostic must never change the outcome */
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Origins b and c: a token WAS resolved. Log which of its three keys exist (presence only), fire-and-forget. */
|
|
198
|
+
function logNoSessionWithKeys(origin, sessionToken) {
|
|
199
|
+
try {
|
|
200
|
+
void (0, session_store_1.describeSessionKeyPresence)(sessionToken).then((keys) => {
|
|
201
|
+
try {
|
|
202
|
+
console.warn(`[server-auth] NO_SESSION origin=${origin}`, { token: (0, token_log_id_1.tokenLogId)(sessionToken), ...keys });
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* never throws */
|
|
206
|
+
}
|
|
207
|
+
}, () => {
|
|
208
|
+
/* never throws */
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
/* a diagnostic must never change the outcome */
|
|
213
|
+
}
|
|
214
|
+
}
|
|
141
215
|
/**
|
|
142
216
|
* Get the current session from a request.
|
|
143
217
|
*
|
|
@@ -167,7 +241,9 @@ async function getSession(request) {
|
|
|
167
241
|
try {
|
|
168
242
|
betterAuthSession = await auth.api.getSession({ headers: request.headers });
|
|
169
243
|
}
|
|
170
|
-
catch {
|
|
244
|
+
catch (err) {
|
|
245
|
+
logSessionReadSwallowed('primary', err); // fall through to canonical Redis path
|
|
246
|
+
}
|
|
171
247
|
if (betterAuthSession?.session?.token && betterAuthSession?.user) {
|
|
172
248
|
const sessionToken = betterAuthSession.session.token;
|
|
173
249
|
let sessionData = null;
|
|
@@ -176,7 +252,9 @@ async function getSession(request) {
|
|
|
176
252
|
if (!sessionData)
|
|
177
253
|
sessionData = await (0, session_store_1.getBetterAuthSession)(sessionToken);
|
|
178
254
|
}
|
|
179
|
-
catch {
|
|
255
|
+
catch (err) {
|
|
256
|
+
logSessionReadSwallowed('A', err); // Redis unavailable
|
|
257
|
+
}
|
|
180
258
|
if (!sessionData)
|
|
181
259
|
sessionData = buildSessionDataFromAuthSession(betterAuthSession);
|
|
182
260
|
return attachSessionData(betterAuthSession, sessionData, sessionToken);
|
|
@@ -201,7 +279,9 @@ async function getSession(request) {
|
|
|
201
279
|
if (!sessionData)
|
|
202
280
|
sessionData = await (0, session_store_1.getBetterAuthSession)(sessionToken);
|
|
203
281
|
}
|
|
204
|
-
catch {
|
|
282
|
+
catch (err) {
|
|
283
|
+
logSessionReadSwallowed('B', err); // Redis unavailable
|
|
284
|
+
}
|
|
205
285
|
if (!sessionData)
|
|
206
286
|
return null;
|
|
207
287
|
// Synthesize a minimal session shape compatible with downstream callers.
|
|
@@ -282,6 +362,7 @@ async function getFreshIdpToken(request, config) {
|
|
|
282
362
|
const session = await getSession(request);
|
|
283
363
|
const sessionToken = session?.session?.token;
|
|
284
364
|
if (!sessionToken) {
|
|
365
|
+
logNoSessionOriginA(request);
|
|
285
366
|
return { success: false, error: 'NO_SESSION', status: 401, terminal: true };
|
|
286
367
|
}
|
|
287
368
|
const { ensureFreshAccessToken } = await Promise.resolve().then(() => __importStar(require('../lib/ensure-fresh-access-token')));
|
|
@@ -294,6 +375,8 @@ async function getFreshIdpToken(request, config) {
|
|
|
294
375
|
requestId: request?.headers?.get('x-request-id') ?? undefined,
|
|
295
376
|
});
|
|
296
377
|
if (!result.ok) {
|
|
378
|
+
if (result.code === 'NO_SESSION')
|
|
379
|
+
logNoSessionWithKeys('b', sessionToken);
|
|
297
380
|
return {
|
|
298
381
|
success: false,
|
|
299
382
|
error: result.code,
|
|
@@ -306,6 +389,7 @@ async function getFreshIdpToken(request, config) {
|
|
|
306
389
|
}
|
|
307
390
|
const sessionData = await (0, session_store_1.getSession)(sessionToken);
|
|
308
391
|
if (!sessionData) {
|
|
392
|
+
logNoSessionWithKeys('c', sessionToken);
|
|
309
393
|
return { success: false, error: 'NO_SESSION', status: 401, terminal: true };
|
|
310
394
|
}
|
|
311
395
|
return {
|
package/dist/vibe/sessions.d.ts
CHANGED
|
@@ -210,29 +210,14 @@ export declare function warnNonNumericUserId(sessionToken: string | undefined, r
|
|
|
210
210
|
export declare function readSessionRevocationHmac(idpUserId: number): Promise<{
|
|
211
211
|
ok: boolean;
|
|
212
212
|
sessions: IVibeLoginSession[];
|
|
213
|
+
failure?: string;
|
|
213
214
|
}>;
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
* VIBE_CLIENT_ID / VIBE_HMAC_KEY absent). Viable; today's
|
|
221
|
-
* behaviour. Logged once, not per request.
|
|
222
|
-
* 'unknown' - revocation IS configured but the read FAILED (non-OK,
|
|
223
|
-
* timeout, throw). Enforcing middleware fails this CLOSED
|
|
224
|
-
* by default (revocationFailMode 'closed'); a Vibe-less
|
|
225
|
-
* consumer is 'not_configured', not 'unknown', so it is not
|
|
226
|
-
* signed out.
|
|
227
|
-
* 'revoked' - the session's own token hash matches a revoked row, or
|
|
228
|
-
* the user has no active session at all.
|
|
229
|
-
* 'active' - read OK and the session is not revoked.
|
|
230
|
-
*
|
|
231
|
-
* @param idpUserId - The user's IDP ID
|
|
232
|
-
* @param sessionToken - The live session token; hashed to match the stored row.
|
|
233
|
-
* When absent, the user-wide check applies (legacy path).
|
|
234
|
-
*/
|
|
235
|
-
export declare function getRevocationStatus(idpUserId: number, sessionToken?: string): Promise<RevocationStatus>;
|
|
215
|
+
/** True only for the exact code CONCURRENT_LIMIT (nested error.code, top-level code/errorCode); a non-JSON body falls back to the literal. */
|
|
216
|
+
export declare function isConcurrentLimitBody(bodyText: string): boolean;
|
|
217
|
+
/** Retry-After seconds when the proxy kept it (capped at 2 s: this runs in the request path), else 200-400 ms jitter. */
|
|
218
|
+
export declare function revocationRetryDelayMs(response: Response): number;
|
|
219
|
+
/** Which branch of the revocation decision produced the verdict (PAY-2136 reason log; never a value). */
|
|
220
|
+
export type RevocationRule = 'own_row_revoked' | 'legacy_no_active' | 'legacy_recent_2h' | 'none';
|
|
236
221
|
/**
|
|
237
222
|
* Get session statistics for admin dashboard.
|
|
238
223
|
*
|
|
@@ -246,3 +231,6 @@ export declare function getSessionStats(accessToken: string): Promise<{
|
|
|
246
231
|
byDevice: Record<string, number>;
|
|
247
232
|
recentLogins: number;
|
|
248
233
|
}>;
|
|
234
|
+
/** Test/ops hook: forget every cached verdict and in-flight read. */
|
|
235
|
+
export declare function resetRevocationCache(): void;
|
|
236
|
+
export declare function getRevocationStatus(idpUserId: number, sessionToken?: string): Promise<RevocationStatus>;
|