@payez/next-mvp 4.4.6 → 4.4.8
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/api-handlers/account/change-password.js +28 -6
- package/dist/auth/better-auth.d.ts +62 -15
- package/dist/auth/better-auth.js +180 -19
- package/dist/client/better-auth-client.d.ts +101 -956
- package/dist/config/env.d.ts +0 -2
- package/dist/config/env.js +0 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -1
- package/dist/lib/idp-client-config.js +8 -1
- package/dist/lib/never-in-response.d.ts +30 -0
- package/dist/lib/never-in-response.js +71 -0
- package/dist/lib/nextauth-secret.d.ts +51 -0
- package/dist/lib/nextauth-secret.js +141 -0
- package/dist/lib/safe-error-response.d.ts +6 -6
- package/dist/lib/safe-error-response.js +6 -6
- package/dist/lib/two-factor-verify-failure.d.ts +36 -3
- package/dist/lib/two-factor-verify-failure.js +79 -2
- package/dist/middleware/create-middleware.d.ts +2 -0
- package/dist/middleware/create-middleware.js +36 -0
- package/dist/pages/login/page.js +5 -0
- package/dist/pages/verify-code/page.js +4 -2
- package/dist/routes/auth/settings.js +4 -1
- package/dist/server/auth.d.ts +36 -294
- package/dist/server/auth.js +22 -25
- package/dist/vibe/client.d.ts +132 -16
- package/dist/vibe/client.js +288 -82
- package/dist/vibe/document-unwrap.d.ts +50 -0
- package/dist/vibe/document-unwrap.js +78 -0
- package/dist/vibe/errors.d.ts +17 -0
- package/dist/vibe/errors.js +21 -1
- package/dist/vibe/generic.d.ts +2 -49
- package/dist/vibe/generic.js +17 -78
- package/dist/vibe/index.d.ts +1 -1
- package/dist/vibe/index.js +2 -1
- package/package.json +17 -2
- package/src/api-handlers/account/change-password.ts +31 -6
- package/src/auth/better-auth.ts +188 -21
- package/src/config/env.ts +0 -2
- package/src/index.ts +4 -0
- package/src/lib/idp-client-config.ts +8 -1
- package/src/lib/never-in-response.ts +64 -0
- package/src/lib/nextauth-secret.ts +168 -0
- package/src/lib/safe-error-response.ts +6 -6
- package/src/lib/two-factor-verify-failure.ts +86 -5
- package/src/middleware/create-middleware.ts +42 -0
- package/src/pages/login/page.tsx +4 -0
- package/src/pages/verify-code/page.tsx +4 -2
- package/src/routes/auth/settings.ts +5 -1
- package/src/server/auth.ts +23 -29
- package/src/vibe/client.ts +378 -91
- package/src/vibe/document-unwrap.ts +103 -0
- package/src/vibe/errors.ts +24 -0
- package/src/vibe/generic.ts +8 -100
- package/src/vibe/index.ts +1 -0
package/dist/config/env.d.ts
CHANGED
|
@@ -19,8 +19,6 @@ export declare const API_ENDPOINTS: {
|
|
|
19
19
|
readonly profile: "/api/account/profile";
|
|
20
20
|
readonly updateProfile: "/api/account/profile";
|
|
21
21
|
readonly maskedInfo: "/api/account/masked-info";
|
|
22
|
-
readonly sendResetCode: "/api/account/send-reset-code";
|
|
23
|
-
readonly verifyResetCode: "/api/account/verify-reset-code";
|
|
24
22
|
readonly resetPassword: "/api/account/reset-password";
|
|
25
23
|
readonly changePassword: "/api/account/change-password";
|
|
26
24
|
readonly validatePassword: "/api/account/validate-password";
|
package/dist/config/env.js
CHANGED
|
@@ -29,8 +29,6 @@ exports.API_ENDPOINTS = {
|
|
|
29
29
|
profile: '/api/account/profile',
|
|
30
30
|
updateProfile: '/api/account/profile',
|
|
31
31
|
maskedInfo: '/api/account/masked-info',
|
|
32
|
-
sendResetCode: '/api/account/send-reset-code',
|
|
33
|
-
verifyResetCode: '/api/account/verify-reset-code',
|
|
34
32
|
resetPassword: '/api/account/reset-password',
|
|
35
33
|
changePassword: '/api/account/change-password',
|
|
36
34
|
validatePassword: '/api/account/validate-password'
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { AuthProvider, useAuthConfig, useAuthMode, useFederatedProviders, useFed
|
|
|
5
5
|
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
|
+
export { safeCallbackPath, isSafeCallbackPath, resolveCallbackDestination, DEFAULT_CALLBACK } from './auth/safe-callback';
|
|
8
9
|
export { createMvpMiddleware } from './middleware/create-middleware';
|
|
9
10
|
export { UserAvatarMenu, MobileNavDrawer } from './components/account';
|
|
10
11
|
export type { UserAvatarMenuProps, MobileNavDrawerProps, NavItem, NavSection } from './components/account';
|
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.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.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");
|
|
@@ -31,6 +31,13 @@ var route_config_1 = require("./auth/route-config");
|
|
|
31
31
|
Object.defineProperty(exports, "isUnauthenticatedRoute", { enumerable: true, get: function () { return route_config_1.isUnauthenticatedRoute; } });
|
|
32
32
|
Object.defineProperty(exports, "configurePublicRoutes", { enumerable: true, get: function () { return route_config_1.configurePublicRoutes; } });
|
|
33
33
|
Object.defineProperty(exports, "getRouteConfig", { enumerable: true, get: function () { return route_config_1.getRouteConfig; } });
|
|
34
|
+
// The ONE callback-target guard (N7 / PAY-1989): apps import this instead of
|
|
35
|
+
// copying it. Exposed from the main index AND as the subpath ./auth/safe-callback.
|
|
36
|
+
var safe_callback_1 = require("./auth/safe-callback");
|
|
37
|
+
Object.defineProperty(exports, "safeCallbackPath", { enumerable: true, get: function () { return safe_callback_1.safeCallbackPath; } });
|
|
38
|
+
Object.defineProperty(exports, "isSafeCallbackPath", { enumerable: true, get: function () { return safe_callback_1.isSafeCallbackPath; } });
|
|
39
|
+
Object.defineProperty(exports, "resolveCallbackDestination", { enumerable: true, get: function () { return safe_callback_1.resolveCallbackDestination; } });
|
|
40
|
+
Object.defineProperty(exports, "DEFAULT_CALLBACK", { enumerable: true, get: function () { return safe_callback_1.DEFAULT_CALLBACK; } });
|
|
34
41
|
var create_middleware_1 = require("./middleware/create-middleware");
|
|
35
42
|
Object.defineProperty(exports, "createMvpMiddleware", { enumerable: true, get: function () { return create_middleware_1.createMvpMiddleware; } });
|
|
36
43
|
// Account Components
|
|
@@ -81,6 +81,7 @@ exports.clearConfigRedisCache = clearConfigRedisCache;
|
|
|
81
81
|
exports.getEnabledProviders = getEnabledProviders;
|
|
82
82
|
require("server-only");
|
|
83
83
|
const crypto_1 = require("crypto");
|
|
84
|
+
const never_in_response_1 = require("./never-in-response");
|
|
84
85
|
// REDIS IS IMPORTED LAZILY, AND THAT IS LOAD-BEARING.
|
|
85
86
|
// A module-scope `import redis from './redis'` drags ioredis into EVERY bundle that
|
|
86
87
|
// touches this file - including instrumentation.ts. Next compiles the instrumentation
|
|
@@ -148,6 +149,10 @@ function resolveAuthSecret(fromConfig) {
|
|
|
148
149
|
function applyEnvSecrets(config) {
|
|
149
150
|
const auth = resolveAuthSecret(config.authSecret);
|
|
150
151
|
logSecretSource('authSecret', auth.source);
|
|
152
|
+
// PAY-1529: every secret this module resolves is registered as never-in-response HERE, the one place
|
|
153
|
+
// both fetch paths and the Redis read pass through, so the runtime backstop knows the value whichever
|
|
154
|
+
// handler later tries to serialize it.
|
|
155
|
+
(0, never_in_response_1.markNeverInResponse)(auth.value);
|
|
151
156
|
return {
|
|
152
157
|
...config,
|
|
153
158
|
authSecret: auth.value,
|
|
@@ -156,7 +161,9 @@ function applyEnvSecrets(config) {
|
|
|
156
161
|
const fromEnv = process.env[envVar];
|
|
157
162
|
const source = fromEnv ? `env:${envVar}` : p.clientSecret ? 'idp-config' : 'none';
|
|
158
163
|
logSecretSource(`${p.provider} clientSecret`, source);
|
|
159
|
-
|
|
164
|
+
const clientSecret = fromEnv || p.clientSecret;
|
|
165
|
+
(0, never_in_response_1.markNeverInResponse)(clientSecret);
|
|
166
|
+
return { ...p, clientSecret };
|
|
160
167
|
}),
|
|
161
168
|
};
|
|
162
169
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAY-1529 runtime backstop: the signing secret and the OAuth provider client secrets must never be
|
|
3
|
+
* serialized into an HTTP response body.
|
|
4
|
+
*
|
|
5
|
+
* The build-time guard (test/secrets-never-in-response-source-guard.test.cjs) follows values through the
|
|
6
|
+
* source it can see. This is the layer for what it cannot see — a value that crosses a module boundary,
|
|
7
|
+
* a structuredClone / Object.assign copy, a helper in a consumer app. It tracks the secret by VALUE:
|
|
8
|
+
* the loader registers every resolved secret here at the moment it resolves it (applyEnvSecrets), and a
|
|
9
|
+
* response built with `secretSafeJson` / checked with `assertNoServerSecret` FAILS CLOSED — it throws,
|
|
10
|
+
* and the route answers 500 — if any registered value appears anywhere in the serialized body, under any
|
|
11
|
+
* key, at any depth, whatever the code that built it called it.
|
|
12
|
+
*
|
|
13
|
+
* Bounds, stated rather than implied:
|
|
14
|
+
* - Values shorter than MIN_SECRET_LENGTH are not registered. A short string matches innocent bodies and
|
|
15
|
+
* would turn this into an outage generator; real signing and provider secrets are far longer.
|
|
16
|
+
* - It protects responses that go through it. Adoption is per handler; the build-time guard is what
|
|
17
|
+
* covers the handlers that do not.
|
|
18
|
+
* - Nothing here ever logs, returns or throws a secret value.
|
|
19
|
+
*/
|
|
20
|
+
import { NextResponse } from 'next/server';
|
|
21
|
+
export declare const MIN_SECRET_LENGTH = 16;
|
|
22
|
+
/** Record a value that must never appear in a response body. Non-strings and short strings are ignored. */
|
|
23
|
+
export declare function markNeverInResponse(value: unknown): void;
|
|
24
|
+
export declare class SecretInResponseError extends Error {
|
|
25
|
+
constructor(where: string);
|
|
26
|
+
}
|
|
27
|
+
/** Throws SecretInResponseError if any registered secret appears in `body` once serialized. */
|
|
28
|
+
export declare function assertNoServerSecret(body: unknown, where: string): void;
|
|
29
|
+
/** NextResponse.json, refusing any body that carries a registered secret. */
|
|
30
|
+
export declare function secretSafeJson<T>(body: T, init: ResponseInit | undefined, where: string): NextResponse<T>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SecretInResponseError = exports.MIN_SECRET_LENGTH = void 0;
|
|
4
|
+
exports.markNeverInResponse = markNeverInResponse;
|
|
5
|
+
exports.assertNoServerSecret = assertNoServerSecret;
|
|
6
|
+
exports.secretSafeJson = secretSafeJson;
|
|
7
|
+
/**
|
|
8
|
+
* PAY-1529 runtime backstop: the signing secret and the OAuth provider client secrets must never be
|
|
9
|
+
* serialized into an HTTP response body.
|
|
10
|
+
*
|
|
11
|
+
* The build-time guard (test/secrets-never-in-response-source-guard.test.cjs) follows values through the
|
|
12
|
+
* source it can see. This is the layer for what it cannot see — a value that crosses a module boundary,
|
|
13
|
+
* a structuredClone / Object.assign copy, a helper in a consumer app. It tracks the secret by VALUE:
|
|
14
|
+
* the loader registers every resolved secret here at the moment it resolves it (applyEnvSecrets), and a
|
|
15
|
+
* response built with `secretSafeJson` / checked with `assertNoServerSecret` FAILS CLOSED — it throws,
|
|
16
|
+
* and the route answers 500 — if any registered value appears anywhere in the serialized body, under any
|
|
17
|
+
* key, at any depth, whatever the code that built it called it.
|
|
18
|
+
*
|
|
19
|
+
* Bounds, stated rather than implied:
|
|
20
|
+
* - Values shorter than MIN_SECRET_LENGTH are not registered. A short string matches innocent bodies and
|
|
21
|
+
* would turn this into an outage generator; real signing and provider secrets are far longer.
|
|
22
|
+
* - It protects responses that go through it. Adoption is per handler; the build-time guard is what
|
|
23
|
+
* covers the handlers that do not.
|
|
24
|
+
* - Nothing here ever logs, returns or throws a secret value.
|
|
25
|
+
*/
|
|
26
|
+
const server_1 = require("next/server");
|
|
27
|
+
exports.MIN_SECRET_LENGTH = 16;
|
|
28
|
+
const registered = new Set();
|
|
29
|
+
/** Record a value that must never appear in a response body. Non-strings and short strings are ignored. */
|
|
30
|
+
function markNeverInResponse(value) {
|
|
31
|
+
if (typeof value !== 'string' || value.length < exports.MIN_SECRET_LENGTH)
|
|
32
|
+
return;
|
|
33
|
+
registered.add(value);
|
|
34
|
+
// JSON escapes some characters, so the serialized form of a secret containing `"` or `\` differs from
|
|
35
|
+
// the raw value. Register that form too, or such a secret would pass through a JSON body unseen.
|
|
36
|
+
const escaped = JSON.stringify(value).slice(1, -1);
|
|
37
|
+
if (escaped !== value)
|
|
38
|
+
registered.add(escaped);
|
|
39
|
+
}
|
|
40
|
+
class SecretInResponseError extends Error {
|
|
41
|
+
constructor(where) {
|
|
42
|
+
// The message names the route, never the value.
|
|
43
|
+
super(`[PAY-1529] Refused to serialize a server secret into the response of ${where}`);
|
|
44
|
+
this.name = 'SecretInResponseError';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.SecretInResponseError = SecretInResponseError;
|
|
48
|
+
/** Throws SecretInResponseError if any registered secret appears in `body` once serialized. */
|
|
49
|
+
function assertNoServerSecret(body, where) {
|
|
50
|
+
if (registered.size === 0 || body === undefined || body === null)
|
|
51
|
+
return;
|
|
52
|
+
let text;
|
|
53
|
+
try {
|
|
54
|
+
text = typeof body === 'string' ? body : JSON.stringify(body);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Unserializable: NextResponse.json would throw on it anyway. Fail closed rather than guess.
|
|
58
|
+
throw new SecretInResponseError(where);
|
|
59
|
+
}
|
|
60
|
+
if (!text)
|
|
61
|
+
return;
|
|
62
|
+
for (const secret of registered) {
|
|
63
|
+
if (text.includes(secret))
|
|
64
|
+
throw new SecretInResponseError(where);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** NextResponse.json, refusing any body that carries a registered secret. */
|
|
68
|
+
function secretSafeJson(body, init, where) {
|
|
69
|
+
assertNoServerSecret(body, where);
|
|
70
|
+
return server_1.NextResponse.json(body, init);
|
|
71
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveNextAuthSecret - COMPATIBILITY SHIM (restored 2026-09-30, PAY-2005 / N7).
|
|
3
|
+
*
|
|
4
|
+
* ---------------------------------------------------------------------------
|
|
5
|
+
* WHY THIS FILE EXISTS, AND WHY IT IS A SHIM RATHER THAN A FEATURE
|
|
6
|
+
* ---------------------------------------------------------------------------
|
|
7
|
+
* The NextAuth-era broker secret resolver was DELETED in
|
|
8
|
+
* 4d3d29e6 "feat!: Better Auth 4.0.0 — NextAuth fully removed"
|
|
9
|
+
* and it is intentionally NOT coming back as first-class API: this package is
|
|
10
|
+
* Better Auth now, and new apps must not resolve a NextAuth secret at all.
|
|
11
|
+
*
|
|
12
|
+
* It is restored here ONLY because ONE consumer is still mid-migration and cannot
|
|
13
|
+
* take the 4.4.x bump without it:
|
|
14
|
+
*
|
|
15
|
+
* CONSUMER THAT NEEDS THIS: the customer portal
|
|
16
|
+
* (`payez_idp_admin_web` / VibeID-Customer-Portal). It is pinned at
|
|
17
|
+
* @payez/next-mvp 4.0.46 and still runs BOTH next-auth@4.24 and better-auth; its
|
|
18
|
+
* `src/lib/server-session.ts` and `src/lib/session-token.ts` (and 5 route files)
|
|
19
|
+
* import `resolveNextAuthSecret` from here. Without this export, bumping the
|
|
20
|
+
* portal to 4.4.x breaks its build. BAPert ruled (68003) to ship this scoped
|
|
21
|
+
* shim rather than force the portal's full NextAuth->BetterAuth migration
|
|
22
|
+
* tonight. That migration is tracked as its own card; WHEN IT LANDS, DELETE
|
|
23
|
+
* THIS FILE.
|
|
24
|
+
*
|
|
25
|
+
* DO NOT add new consumers. If you are starting an app, use Better Auth's own
|
|
26
|
+
* secret handling; you do not need this.
|
|
27
|
+
*
|
|
28
|
+
* ---------------------------------------------------------------------------
|
|
29
|
+
* BEHAVIOUR (faithful to the 4.0.46 implementation, with one deliberate change)
|
|
30
|
+
* ---------------------------------------------------------------------------
|
|
31
|
+
* Priority: (1) process.env.NEXTAUTH_SECRET if set; (2) fetch from the IDP via a
|
|
32
|
+
* signed client assertion; (3) cache in-memory 5 min and stamp NEXTAUTH_SECRET.
|
|
33
|
+
*
|
|
34
|
+
* DELIBERATE CHANGE, 4.0.46 -> here: 4.0.46 validated the fetched secret with
|
|
35
|
+
* `validateNextAuthSecret`, which this package's 4.4.x no longer exports (it was
|
|
36
|
+
* renamed to `validateAuthSecret` with an IDENTICAL body). The shim calls
|
|
37
|
+
* `validateAuthSecret` - same predicate, current name - so there is no second
|
|
38
|
+
* copy of the rule. The validation semantics are unchanged.
|
|
39
|
+
*
|
|
40
|
+
* SERVER-ONLY: the IDP handshake and NEXTAUTH_SECRET are server concerns.
|
|
41
|
+
*/
|
|
42
|
+
import 'server-only';
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the NextAuth secret (server-only).
|
|
45
|
+
*
|
|
46
|
+
* Priority:
|
|
47
|
+
* 1) Use process.env.NEXTAUTH_SECRET if present (allows overrides/production)
|
|
48
|
+
* 2) Fetch from the IDP broker endpoint - the IDP handles all Key Vault/signing
|
|
49
|
+
* 3) Cache result in-memory and set process.env.NEXTAUTH_SECRET for later calls
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveNextAuthSecret(): Promise<string>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveNextAuthSecret = resolveNextAuthSecret;
|
|
4
|
+
/**
|
|
5
|
+
* resolveNextAuthSecret - COMPATIBILITY SHIM (restored 2026-09-30, PAY-2005 / N7).
|
|
6
|
+
*
|
|
7
|
+
* ---------------------------------------------------------------------------
|
|
8
|
+
* WHY THIS FILE EXISTS, AND WHY IT IS A SHIM RATHER THAN A FEATURE
|
|
9
|
+
* ---------------------------------------------------------------------------
|
|
10
|
+
* The NextAuth-era broker secret resolver was DELETED in
|
|
11
|
+
* 4d3d29e6 "feat!: Better Auth 4.0.0 — NextAuth fully removed"
|
|
12
|
+
* and it is intentionally NOT coming back as first-class API: this package is
|
|
13
|
+
* Better Auth now, and new apps must not resolve a NextAuth secret at all.
|
|
14
|
+
*
|
|
15
|
+
* It is restored here ONLY because ONE consumer is still mid-migration and cannot
|
|
16
|
+
* take the 4.4.x bump without it:
|
|
17
|
+
*
|
|
18
|
+
* CONSUMER THAT NEEDS THIS: the customer portal
|
|
19
|
+
* (`payez_idp_admin_web` / VibeID-Customer-Portal). It is pinned at
|
|
20
|
+
* @payez/next-mvp 4.0.46 and still runs BOTH next-auth@4.24 and better-auth; its
|
|
21
|
+
* `src/lib/server-session.ts` and `src/lib/session-token.ts` (and 5 route files)
|
|
22
|
+
* import `resolveNextAuthSecret` from here. Without this export, bumping the
|
|
23
|
+
* portal to 4.4.x breaks its build. BAPert ruled (68003) to ship this scoped
|
|
24
|
+
* shim rather than force the portal's full NextAuth->BetterAuth migration
|
|
25
|
+
* tonight. That migration is tracked as its own card; WHEN IT LANDS, DELETE
|
|
26
|
+
* THIS FILE.
|
|
27
|
+
*
|
|
28
|
+
* DO NOT add new consumers. If you are starting an app, use Better Auth's own
|
|
29
|
+
* secret handling; you do not need this.
|
|
30
|
+
*
|
|
31
|
+
* ---------------------------------------------------------------------------
|
|
32
|
+
* BEHAVIOUR (faithful to the 4.0.46 implementation, with one deliberate change)
|
|
33
|
+
* ---------------------------------------------------------------------------
|
|
34
|
+
* Priority: (1) process.env.NEXTAUTH_SECRET if set; (2) fetch from the IDP via a
|
|
35
|
+
* signed client assertion; (3) cache in-memory 5 min and stamp NEXTAUTH_SECRET.
|
|
36
|
+
*
|
|
37
|
+
* DELIBERATE CHANGE, 4.0.46 -> here: 4.0.46 validated the fetched secret with
|
|
38
|
+
* `validateNextAuthSecret`, which this package's 4.4.x no longer exports (it was
|
|
39
|
+
* renamed to `validateAuthSecret` with an IDENTICAL body). The shim calls
|
|
40
|
+
* `validateAuthSecret` - same predicate, current name - so there is no second
|
|
41
|
+
* copy of the rule. The validation semantics are unchanged.
|
|
42
|
+
*
|
|
43
|
+
* SERVER-ONLY: the IDP handshake and NEXTAUTH_SECRET are server concerns.
|
|
44
|
+
*/
|
|
45
|
+
require("server-only");
|
|
46
|
+
const node_crypto_1 = require("node:crypto");
|
|
47
|
+
const secret_validation_1 = require("./secret-validation");
|
|
48
|
+
let cachedSecret = null;
|
|
49
|
+
let lastFetchedAt = 0;
|
|
50
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the NextAuth secret (server-only).
|
|
53
|
+
*
|
|
54
|
+
* Priority:
|
|
55
|
+
* 1) Use process.env.NEXTAUTH_SECRET if present (allows overrides/production)
|
|
56
|
+
* 2) Fetch from the IDP broker endpoint - the IDP handles all Key Vault/signing
|
|
57
|
+
* 3) Cache result in-memory and set process.env.NEXTAUTH_SECRET for later calls
|
|
58
|
+
*/
|
|
59
|
+
async function resolveNextAuthSecret() {
|
|
60
|
+
// Check if already in environment
|
|
61
|
+
if (process.env.NEXTAUTH_SECRET && process.env.NEXTAUTH_SECRET.trim() !== '') {
|
|
62
|
+
return process.env.NEXTAUTH_SECRET;
|
|
63
|
+
}
|
|
64
|
+
// Check if cached and fresh (within 5 minutes)
|
|
65
|
+
if (cachedSecret && Date.now() - lastFetchedAt < CACHE_TTL_MS) {
|
|
66
|
+
return cachedSecret;
|
|
67
|
+
}
|
|
68
|
+
// Broker mode: fetch from the IDP (the IDP handles all Key Vault/signing)
|
|
69
|
+
const base = process.env.IDP_URL;
|
|
70
|
+
if (!base)
|
|
71
|
+
throw new Error('IDP_URL environment variable is required');
|
|
72
|
+
const clientIdStr = process.env.CLIENT_ID;
|
|
73
|
+
if (!clientIdStr || clientIdStr.trim() === '') {
|
|
74
|
+
throw new Error('CLIENT_ID is required (e.g., "ideal_resume_website")');
|
|
75
|
+
}
|
|
76
|
+
// Step 1: ask the IDP to sign a client assertion (the IDP holds the keys, not us)
|
|
77
|
+
const signingUrl = `${base.replace(/\/$/, '')}/api/ExternalAuth/sign-client-assertion`;
|
|
78
|
+
const signingResp = await fetch(signingUrl, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
Accept: 'application/json',
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
'X-Client-Id': clientIdStr,
|
|
84
|
+
'X-Correlation-Id': (0, node_crypto_1.randomUUID)().replace(/-/g, ''),
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
issuer: clientIdStr,
|
|
88
|
+
subject: clientIdStr,
|
|
89
|
+
audience: 'urn:payez:externalauth:nextauthsecret',
|
|
90
|
+
expires_in: 60,
|
|
91
|
+
}),
|
|
92
|
+
cache: 'no-store',
|
|
93
|
+
});
|
|
94
|
+
if (!signingResp.ok) {
|
|
95
|
+
const txt = await signingResp.text().catch(() => 'Unknown error');
|
|
96
|
+
throw new Error(`Failed to sign client assertion: ${signingResp.status} ${signingResp.statusText} - ${txt}`);
|
|
97
|
+
}
|
|
98
|
+
const signingBody = (await signingResp.json().catch(() => ({})));
|
|
99
|
+
const clientAssertion = signingBody?.data?.client_assertion ??
|
|
100
|
+
signingBody?.data?.clientAssertion ??
|
|
101
|
+
signingBody?.client_assertion ??
|
|
102
|
+
signingBody?.clientAssertion ??
|
|
103
|
+
signingBody?.data?.ClientAssertion ??
|
|
104
|
+
signingBody?.ClientAssertion;
|
|
105
|
+
if (!clientAssertion || typeof clientAssertion !== 'string') {
|
|
106
|
+
throw new Error('IDP did not return a valid signed client assertion');
|
|
107
|
+
}
|
|
108
|
+
// Step 2: use the signed assertion to fetch the NextAuth secret
|
|
109
|
+
const proxyUrl = `${base.replace(/\/$/, '')}/api/ExternalAuth/next-auth/secret`;
|
|
110
|
+
const proxyResp = await fetch(proxyUrl, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {
|
|
113
|
+
Accept: 'application/json',
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
'X-Client-Id': clientIdStr,
|
|
116
|
+
'X-Correlation-Id': (0, node_crypto_1.randomUUID)().replace(/-/g, ''),
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({ client_assertion: clientAssertion }),
|
|
119
|
+
cache: 'no-store',
|
|
120
|
+
});
|
|
121
|
+
if (!proxyResp.ok) {
|
|
122
|
+
const txt = await proxyResp.text().catch(() => 'Unknown error');
|
|
123
|
+
throw new Error(`Proxy error: ${proxyResp.status} ${proxyResp.statusText} - ${txt}`);
|
|
124
|
+
}
|
|
125
|
+
const proxyBody = (await proxyResp.json().catch(() => ({})));
|
|
126
|
+
const secret = proxyBody?.data?.secret ?? proxyBody?.secret;
|
|
127
|
+
// `configuration` is available from the IDP but deliberately not logged.
|
|
128
|
+
if (!secret || typeof secret !== 'string') {
|
|
129
|
+
throw new Error('Proxy did not return a valid NextAuth secret');
|
|
130
|
+
}
|
|
131
|
+
// Same predicate as 4.0.46's validateNextAuthSecret, under its current name.
|
|
132
|
+
const validation = (0, secret_validation_1.validateAuthSecret)(secret);
|
|
133
|
+
if (!validation.valid) {
|
|
134
|
+
throw new Error(`Fetched NextAuth secret failed validation: ${validation.reason}`);
|
|
135
|
+
}
|
|
136
|
+
cachedSecret = secret;
|
|
137
|
+
lastFetchedAt = Date.now();
|
|
138
|
+
process.env.NEXTAUTH_SECRET = secret;
|
|
139
|
+
console.log('[NEXTAUTH-SECRET] Resolved from IDP (length:', secret.length + ')');
|
|
140
|
+
return secret;
|
|
141
|
+
}
|
|
@@ -50,12 +50,6 @@ export declare function idpErrorEnvelope(json: unknown, fallbackMessage?: string
|
|
|
50
50
|
};
|
|
51
51
|
request_id?: string;
|
|
52
52
|
};
|
|
53
|
-
/**
|
|
54
|
-
* PAY-1591: the IdP refuses a reset password with error.details = { code: 'password_policy', rules: [codes], requirements:
|
|
55
|
-
* [English] }. This route is anonymous, so only the CODES cross, and only the known ones: never `requirements`, never an
|
|
56
|
-
* unknown string. Returns undefined when this is NOT a password-policy refusal, and [] when it is one but names no rule we know
|
|
57
|
-
* (the caller must be able to tell a refused password from a dead recovery session).
|
|
58
|
-
*/
|
|
59
53
|
/**
|
|
60
54
|
* True only when the IdP EXPLICITLY says the recovery token itself is unusable: details.code token_expired / token_used, or an
|
|
61
55
|
* ASP.NET Identity `InvalidToken` error in details.value (what the served IdP sends when ResetPasswordAsync refuses a used or
|
|
@@ -64,4 +58,10 @@ export declare function idpErrorEnvelope(json: unknown, fallbackMessage?: string
|
|
|
64
58
|
* no code at all, and calling that 'link expired' loops the user.
|
|
65
59
|
*/
|
|
66
60
|
export declare function projectResetTokenInvalid(json: unknown): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* PAY-1591: the IdP refuses a reset password with error.details = { code: 'password_policy', rules: [codes], requirements:
|
|
63
|
+
* [English] }. This route is anonymous, so only the CODES cross, and only the known ones: never `requirements`, never an
|
|
64
|
+
* unknown string. Returns undefined when this is NOT a password-policy refusal, and [] when it is one but names no rule we know
|
|
65
|
+
* (the caller must be able to tell a refused password from a dead recovery session).
|
|
66
|
+
*/
|
|
67
67
|
export declare function projectPasswordPolicyRules(json: unknown): string[] | undefined;
|
|
@@ -93,12 +93,6 @@ function idpErrorEnvelope(json, fallbackMessage = 'Request failed') {
|
|
|
93
93
|
out.request_id = p.request_id;
|
|
94
94
|
return out;
|
|
95
95
|
}
|
|
96
|
-
/**
|
|
97
|
-
* PAY-1591: the IdP refuses a reset password with error.details = { code: 'password_policy', rules: [codes], requirements:
|
|
98
|
-
* [English] }. This route is anonymous, so only the CODES cross, and only the known ones: never `requirements`, never an
|
|
99
|
-
* unknown string. Returns undefined when this is NOT a password-policy refusal, and [] when it is one but names no rule we know
|
|
100
|
-
* (the caller must be able to tell a refused password from a dead recovery session).
|
|
101
|
-
*/
|
|
102
96
|
/**
|
|
103
97
|
* True only when the IdP EXPLICITLY says the recovery token itself is unusable: details.code token_expired / token_used, or an
|
|
104
98
|
* ASP.NET Identity `InvalidToken` error in details.value (what the served IdP sends when ResetPasswordAsync refuses a used or
|
|
@@ -118,6 +112,12 @@ function projectResetTokenInvalid(json) {
|
|
|
118
112
|
return true;
|
|
119
113
|
return Array.isArray(details.value) && details.value.some((v) => !!v && typeof v === 'object' && v.code === 'InvalidToken');
|
|
120
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* PAY-1591: the IdP refuses a reset password with error.details = { code: 'password_policy', rules: [codes], requirements:
|
|
117
|
+
* [English] }. This route is anonymous, so only the CODES cross, and only the known ones: never `requirements`, never an
|
|
118
|
+
* unknown string. Returns undefined when this is NOT a password-policy refusal, and [] when it is one but names no rule we know
|
|
119
|
+
* (the caller must be able to tell a refused password from a dead recovery session).
|
|
120
|
+
*/
|
|
121
121
|
function projectPasswordPolicyRules(json) {
|
|
122
122
|
if (!json || typeof json !== 'object')
|
|
123
123
|
return undefined;
|
|
@@ -18,7 +18,38 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { NextResponse } from 'next/server';
|
|
20
20
|
export type TwoFactorVerifyMethod = 'totp' | 'sms' | 'email' | 'backup_code';
|
|
21
|
-
export type TwoFactorVerifyFailureCode = 'INVALID_CODE' | 'LOCKED' | 'RATE_LIMITED' | 'SESSION_REJECTED' | 'VERIFY_UNAVAILABLE' | 'VERIFY_FAILED';
|
|
21
|
+
export type TwoFactorVerifyFailureCode = 'INVALID_CODE' | 'LOCKED' | 'RATE_LIMITED' | 'SESSION_REJECTED' | 'STEP_UP_FACTOR_NOT_ALLOWED' | 'VERIFY_UNAVAILABLE' | 'VERIFY_FAILED';
|
|
22
|
+
/**
|
|
23
|
+
* PAY-1611: the IdP's 403 STEP_UP_FACTOR_NOT_ALLOWED refusal (Core ExternalAuthController.DeliveredCodeStepUpRefusal).
|
|
24
|
+
* A session signed in with an emailed/texted code only cannot re-prove itself with another emailed/texted code; the
|
|
25
|
+
* IdP says which factors it WILL take and what the user can do instead. The portal can only offer those options if
|
|
26
|
+
* this BFF relays them, so on that one refusal we emit the code plus ALLOWLISTED copies of the two lists.
|
|
27
|
+
*
|
|
28
|
+
* On the wire PayEzControllerBase.OnActionExecuted re-envelopes the action's object: the code is `error.code` and the
|
|
29
|
+
* object sits under `error.details`, so the lists are `error.details.allowed_methods` / `error.details.next_steps`.
|
|
30
|
+
* The unenveloped action shape (top-level `error` string, lists at top level) is accepted too.
|
|
31
|
+
*
|
|
32
|
+
* NEVER relayed (PAY-1443 stands): `error_description` or any other IdP prose, and any IdP `endpoint` - the consumer
|
|
33
|
+
* maps each action to its OWN route from a constant table. Unknown methods/actions are dropped, not passed through;
|
|
34
|
+
* missing or malformed lists give EMPTY lists, never invented ones.
|
|
35
|
+
*/
|
|
36
|
+
export declare const STEP_UP_REFUSAL_CODE = "STEP_UP_FACTOR_NOT_ALLOWED";
|
|
37
|
+
declare const STEP_UP_METHODS: readonly ["totp", "webauthn", "backup_code"];
|
|
38
|
+
declare const STEP_UP_ACTIONS: readonly ["use_authenticator", "reset_password", "enroll_authenticator"];
|
|
39
|
+
export type StepUpMethod = (typeof STEP_UP_METHODS)[number];
|
|
40
|
+
export type StepUpAction = (typeof STEP_UP_ACTIONS)[number];
|
|
41
|
+
export type StepUpNextStep = {
|
|
42
|
+
action: 'use_authenticator';
|
|
43
|
+
methods: StepUpMethod[];
|
|
44
|
+
} | {
|
|
45
|
+
action: Exclude<StepUpAction, 'use_authenticator'>;
|
|
46
|
+
};
|
|
47
|
+
export type StepUpRefusal = {
|
|
48
|
+
allowed_methods: StepUpMethod[];
|
|
49
|
+
next_steps: StepUpNextStep[];
|
|
50
|
+
};
|
|
51
|
+
/** The step-up refusal's allowlisted lists, or null when this is not that refusal (status 403 + the exact code only). */
|
|
52
|
+
export declare function stepUpRefusal(status: number, json: unknown): StepUpRefusal | null;
|
|
22
53
|
/**
|
|
23
54
|
* Every string the IDP might have put anywhere in the error envelope, unwrapped from
|
|
24
55
|
* `{ message = X }` ToString form. Classification input only.
|
|
@@ -37,9 +68,11 @@ export declare function twoFactorVerifyFailure(result: {
|
|
|
37
68
|
* so no `message` field is emitted on failure.
|
|
38
69
|
*/
|
|
39
70
|
export type TwoFactorSendMethod = 'sms' | 'email';
|
|
40
|
-
export type TwoFactorSendFailureCode = 'RATE_LIMITED' | 'SESSION_REJECTED' | 'SEND_UNAVAILABLE' | 'SEND_FAILED';
|
|
41
|
-
export declare function classifyTwoFactorSendFailure(status: number): TwoFactorSendFailureCode;
|
|
71
|
+
export type TwoFactorSendFailureCode = 'RATE_LIMITED' | 'SESSION_REJECTED' | 'STEP_UP_FACTOR_NOT_ALLOWED' | 'SEND_UNAVAILABLE' | 'SEND_FAILED';
|
|
72
|
+
export declare function classifyTwoFactorSendFailure(status: number, json?: unknown): TwoFactorSendFailureCode;
|
|
42
73
|
export declare function twoFactorSendCodeFailure(result: {
|
|
43
74
|
status: number;
|
|
44
75
|
attemptedRefresh: boolean;
|
|
76
|
+
json?: unknown;
|
|
45
77
|
}, method: TwoFactorSendMethod): NextResponse;
|
|
78
|
+
export {};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.STEP_UP_REFUSAL_CODE = void 0;
|
|
4
|
+
exports.stepUpRefusal = stepUpRefusal;
|
|
3
5
|
exports.upstreamErrorTexts = upstreamErrorTexts;
|
|
4
6
|
exports.classifyTwoFactorVerifyFailure = classifyTwoFactorVerifyFailure;
|
|
5
7
|
exports.twoFactorVerifyFailure = twoFactorVerifyFailure;
|
|
@@ -61,6 +63,70 @@ const IDP_LOCKED = /^locked$|code\s*=\s*locked/i;
|
|
|
61
63
|
*/
|
|
62
64
|
const IDP_UNAVAILABLE = /^(?:service_unavailable|server_error)$|code\s*=\s*(?:service_unavailable|server_error)\b/i;
|
|
63
65
|
const WRONG_CODE = /\b(invalid|incorrect|wrong|expired)\b[^.]{0,40}\b(code|otp|totp|token)\b|\binvalid_code\b/i;
|
|
66
|
+
/**
|
|
67
|
+
* PAY-1611: the IdP's 403 STEP_UP_FACTOR_NOT_ALLOWED refusal (Core ExternalAuthController.DeliveredCodeStepUpRefusal).
|
|
68
|
+
* A session signed in with an emailed/texted code only cannot re-prove itself with another emailed/texted code; the
|
|
69
|
+
* IdP says which factors it WILL take and what the user can do instead. The portal can only offer those options if
|
|
70
|
+
* this BFF relays them, so on that one refusal we emit the code plus ALLOWLISTED copies of the two lists.
|
|
71
|
+
*
|
|
72
|
+
* On the wire PayEzControllerBase.OnActionExecuted re-envelopes the action's object: the code is `error.code` and the
|
|
73
|
+
* object sits under `error.details`, so the lists are `error.details.allowed_methods` / `error.details.next_steps`.
|
|
74
|
+
* The unenveloped action shape (top-level `error` string, lists at top level) is accepted too.
|
|
75
|
+
*
|
|
76
|
+
* NEVER relayed (PAY-1443 stands): `error_description` or any other IdP prose, and any IdP `endpoint` - the consumer
|
|
77
|
+
* maps each action to its OWN route from a constant table. Unknown methods/actions are dropped, not passed through;
|
|
78
|
+
* missing or malformed lists give EMPTY lists, never invented ones.
|
|
79
|
+
*/
|
|
80
|
+
exports.STEP_UP_REFUSAL_CODE = 'STEP_UP_FACTOR_NOT_ALLOWED';
|
|
81
|
+
const STEP_UP_METHODS = ['totp', 'webauthn', 'backup_code'];
|
|
82
|
+
const STEP_UP_ACTIONS = ['use_authenticator', 'reset_password', 'enroll_authenticator'];
|
|
83
|
+
const STEP_UP_TEXT = 'This session was signed in with an emailed or texted code only, so another emailed or texted code cannot confirm it is you.';
|
|
84
|
+
function allowedMethods(v) {
|
|
85
|
+
if (!Array.isArray(v))
|
|
86
|
+
return [];
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const m of v) {
|
|
89
|
+
if (STEP_UP_METHODS.includes(m) && !out.includes(m))
|
|
90
|
+
out.push(m);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/** The step-up refusal's allowlisted lists, or null when this is not that refusal (status 403 + the exact code only). */
|
|
95
|
+
function stepUpRefusal(status, json) {
|
|
96
|
+
if (status !== 403 || !json || typeof json !== 'object')
|
|
97
|
+
return null;
|
|
98
|
+
const j = json;
|
|
99
|
+
const err = j.error;
|
|
100
|
+
let code;
|
|
101
|
+
let src = j;
|
|
102
|
+
if (err && typeof err === 'object') {
|
|
103
|
+
const e = err;
|
|
104
|
+
code = e.code;
|
|
105
|
+
src = e.details && typeof e.details === 'object' ? e.details : {};
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
code = err;
|
|
109
|
+
}
|
|
110
|
+
if (code !== exports.STEP_UP_REFUSAL_CODE)
|
|
111
|
+
return null;
|
|
112
|
+
const next_steps = [];
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
for (const step of Array.isArray(src.next_steps) ? src.next_steps : []) {
|
|
115
|
+
if (!step || typeof step !== 'object')
|
|
116
|
+
continue;
|
|
117
|
+
const action = step.action;
|
|
118
|
+
if (typeof action !== 'string' || !STEP_UP_ACTIONS.includes(action) || seen.has(action))
|
|
119
|
+
continue;
|
|
120
|
+
seen.add(action);
|
|
121
|
+
next_steps.push(action === 'use_authenticator'
|
|
122
|
+
? { action, methods: allowedMethods(step.methods) }
|
|
123
|
+
: { action: action });
|
|
124
|
+
}
|
|
125
|
+
return { allowed_methods: allowedMethods(src.allowed_methods), next_steps };
|
|
126
|
+
}
|
|
127
|
+
function stepUpResponse(refusal, attemptedRefresh) {
|
|
128
|
+
return server_1.NextResponse.json({ success: false, error: STEP_UP_TEXT, code: exports.STEP_UP_REFUSAL_CODE, ...refusal, meta: { attemptedRefresh } }, { status: 403 });
|
|
129
|
+
}
|
|
64
130
|
/**
|
|
65
131
|
* Every string the IDP might have put anywhere in the error envelope, unwrapped from
|
|
66
132
|
* `{ message = X }` ToString form. Classification input only.
|
|
@@ -95,6 +161,9 @@ function upstreamErrorTexts(json) {
|
|
|
95
161
|
return out;
|
|
96
162
|
}
|
|
97
163
|
function classifyTwoFactorVerifyFailure(status, json) {
|
|
164
|
+
// PAY-1611: checked FIRST - an exact code on a 403, so no prose rule below can claim it first.
|
|
165
|
+
if (stepUpRefusal(status, json))
|
|
166
|
+
return 'STEP_UP_FACTOR_NOT_ALLOWED';
|
|
98
167
|
// PAY-902 (BAPert R484): the IdP's lockout CODE, not its prose. Door A is post-authentication, so stating
|
|
99
168
|
// the lockout discloses nothing; telling a locked-out user "try again" sends them to spend attempts.
|
|
100
169
|
//
|
|
@@ -118,12 +187,17 @@ function classifyTwoFactorVerifyFailure(status, json) {
|
|
|
118
187
|
return 'VERIFY_FAILED';
|
|
119
188
|
}
|
|
120
189
|
function twoFactorVerifyFailure(result, method) {
|
|
190
|
+
const refusal = stepUpRefusal(result.status, result.json);
|
|
191
|
+
if (refusal)
|
|
192
|
+
return stepUpResponse(refusal, result.attemptedRefresh);
|
|
121
193
|
const code = classifyTwoFactorVerifyFailure(result.status, result.json);
|
|
122
194
|
const error = code === 'INVALID_CODE' ? INVALID_CODE_TEXT[method] : FIXED_TEXT[code];
|
|
123
195
|
return server_1.NextResponse.json({ success: false, error, code, meta: { attemptedRefresh: result.attemptedRefresh } }, { status: result.status });
|
|
124
196
|
}
|
|
125
197
|
const SEND_LABEL = { sms: 'text message', email: 'email' };
|
|
126
|
-
function classifyTwoFactorSendFailure(status) {
|
|
198
|
+
function classifyTwoFactorSendFailure(status, json) {
|
|
199
|
+
if (stepUpRefusal(status, json))
|
|
200
|
+
return 'STEP_UP_FACTOR_NOT_ALLOWED'; // PAY-1611, before the 401/403 branch
|
|
127
201
|
if (status === 429)
|
|
128
202
|
return 'RATE_LIMITED';
|
|
129
203
|
if (status === 401 || status === 403)
|
|
@@ -133,7 +207,10 @@ function classifyTwoFactorSendFailure(status) {
|
|
|
133
207
|
return 'SEND_FAILED';
|
|
134
208
|
}
|
|
135
209
|
function twoFactorSendCodeFailure(result, method) {
|
|
136
|
-
const
|
|
210
|
+
const refusal = stepUpRefusal(result.status, result.json);
|
|
211
|
+
if (refusal)
|
|
212
|
+
return stepUpResponse(refusal, result.attemptedRefresh);
|
|
213
|
+
const code = classifyTwoFactorSendFailure(result.status, result.json);
|
|
137
214
|
const label = SEND_LABEL[method];
|
|
138
215
|
const error = code === 'RATE_LIMITED' ? FIXED_TEXT.RATE_LIMITED
|
|
139
216
|
: code === 'SESSION_REJECTED' ? `We could not send a ${label} code for this session. Please try again.`
|
|
@@ -100,4 +100,6 @@ export interface MvpMiddlewareOptions {
|
|
|
100
100
|
/** Paths exempt from RBAC checks (auth still enforced, just no page-permission check) */
|
|
101
101
|
rbacExemptPaths?: string[];
|
|
102
102
|
}
|
|
103
|
+
/** Login error code for a pre-2FA session that expired before the code was verified (PAY-884). */
|
|
104
|
+
export declare const TWO_FACTOR_TIMEOUT_ERROR = "TwoFactorTimeout";
|
|
103
105
|
export declare function createMvpMiddleware(options?: MvpMiddlewareOptions): (request: NextRequest) => Promise<NextResponse>;
|